diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac5..c474998e6fd3db 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 180) { +kibanaPipeline(timeoutMinutes: 240) { catchErrors { withEnv([ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. diff --git a/.eslintignore b/.eslintignore index 1f22b6074e76e0..2ed9ecf971ff35 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,46 +1,48 @@ -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/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 dfb4603ba95afc..c9b41ec711b7f9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -217,6 +217,9 @@ module.exports = { '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)/**/*', @@ -562,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': [ @@ -611,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', @@ -739,6 +742,101 @@ 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 */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0d86726dca836c..a97400ee09c0e9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,7 +11,7 @@ /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 @@ -84,7 +84,6 @@ /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/uptime @elastic/uptime # Machine Learning @@ -167,8 +166,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 @@ -176,7 +173,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 @@ -209,22 +205,21 @@ /x-pack/plugins/watcher/ @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/.i18nrc.json b/.i18nrc.json index 35ce7452343466..b04c02f6b22655 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -50,10 +50,10 @@ "visTypeMetric": "src/plugins/vis_type_metric", "visTypeTable": "src/plugins/vis_type_table", "visTypeTagCloud": "src/plugins/vis_type_tagcloud", - "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], + "visTypeTimeseries": "src/plugins/vis_type_timeseries", "visTypeVega": "src/plugins/vis_type_vega", - "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", - "visTypeXy": "src/legacy/core_plugins/vis_type_xy", + "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 89735342a2d6f7..44b4d493841361 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -2,9 +2,8 @@ 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' diff --git a/Jenkinsfile b/Jenkinsfile index 6646ee15ba1c29..958ced8c6195a6 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 447a4c6d2601b4..8f356abd095f2b 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 00000000000000..4cd4f31e582db2 --- /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 51b5273851ce7e..2b091d9abb9fce 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/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5450e84417f89b..a91a5bec988b7a 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.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md index 47feb50e9a827d..6d4e252fe7532e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -4,6 +4,7 @@ ## SavedObjectSanitizedDoc type +Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a `references` root property. Signature: 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 c39df0655f1d41..610356a733126e 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 96f11436308569..dbc7d0ca431cec 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 00000000000000..627ea3695383a0 --- /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 6350afacee2bad..00000000000000 --- 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 bdffff5c1365b8..00000000000000 --- 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 250b9d38996702..7874be311d52ce 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 00000000000000..be51400addbbc5 --- /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.field.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md new file mode 100644 index 00000000000000..43ff9a930b974d --- /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 00000000000000..c3b2ac8d30b5a9 --- /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 00000000000000..fcfd7d73c8b0ce --- /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 00000000000000..21b6917c4aad41 --- /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 00000000000000..4f51d88a3046ed --- /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 00000000000000..0846a7595cf907 --- /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 00000000000000..efe1bceb43361a --- /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 00000000000000..fd7be589e87a79 --- /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 00000000000000..431e043d1fecc6 --- /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 00000000000000..59420747e0958e --- /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 00000000000000..d51857090356fe --- /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 00000000000000..86ff2b2c28ae94 --- /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 00000000000000..d2a9b9b86aefc9 --- /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 00000000000000..676ff9bdfc35a7 --- /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 00000000000000..1f6c8105e3f618 --- /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 00000000000000..186d344f503788 --- /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 00000000000000..0cd4b14d0e1e5c --- /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 00000000000000..bef3b2131fa475 --- /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 00000000000000..490615edcf097e --- /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 00000000000000..f32a5c456dc5db --- /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 00000000000000..e38da6600696c9 --- /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 00000000000000..ac3f256a9afc30 --- /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 00000000000000..0535585cb4718c --- /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 00000000000000..436124ac083871 --- /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 00000000000000..1d109a599d2d9c --- /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 00000000000000..ec497de59d2368 --- /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 00000000000000..446e0c237ce132 --- /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 00000000000000..f4eeb5eed06a09 --- /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 00000000000000..59afdc25df350a --- /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 00000000000000..945ac7ededff64 --- /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 00000000000000..91c3ff4f2d9a30 --- /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 00000000000000..c6afa27fe59528 --- /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 00000000000000..b53e301c46c1cf --- /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 00000000000000..1e7fd9d161429b --- /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 00000000000000..5825af4925d066 --- /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 00000000000000..41f5f2446f22aa --- /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 00000000000000..57ccca9136081f --- /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 00000000000000..b19246758f080b --- /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 00000000000000..5fa7d4841537b9 --- /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 00000000000000..394a2e3cc9afbf --- /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.indexpatternfield.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md deleted file mode 100644 index f52a3324af36f7..00000000000000 --- 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 cf7470c035a533..00000000000000 --- 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 267c8f786b5dd2..00000000000000 --- 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 8e848276f21c4c..00000000000000 --- 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 ed9630f92fc975..00000000000000 --- 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 dec74df099d43a..00000000000000 --- 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 4290c4a2f86b3b..00000000000000 --- 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 d5df8ed628cb08..00000000000000 --- 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 d1a1ee0905c6e3..00000000000000 --- 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 f731be8f613cfa..00000000000000 --- 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 df0de6ce0e5419..00000000000000 --- 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 cb24621e732093..00000000000000 --- 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 132ba25a476374..00000000000000 --- 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 1dd6bc865a75d9..00000000000000 --- 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 42f984d851435b..00000000000000 --- 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 72d225185140be..00000000000000 --- 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 2d807f8a5739ce..00000000000000 --- 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 c8483c9b83c9a1..00000000000000 --- 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 dd661ae779c117..00000000000000 --- 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.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 604ac5120922bf..0fd82ffb2240c8 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 @@ -11,9 +11,10 @@ | [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) | | 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 4d7a0b3cfbbca5..bd617990a00a2f 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").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; @@ -26,7 +26,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { `{ fieldFormats: { - register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }` diff --git a/docs/logs/images/logs-action-menu.png b/docs/logs/images/logs-action-menu.png new file mode 100644 index 00000000000000..f1c79b6fa88d14 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 00000000000000..09a9e89fc30429 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 96e7c5fa4d3128..4f5992f945c7aa 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 7081590931a992..51910169e8673a 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] diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index b71e1c672756ac..36578c909f5131 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -19,10 +19,4 @@ 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]. -[[data-visualizer-settings]] -==== {data-viz} settings - -`xpack.ml.file_data_visualizer.max_file_size`:: -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`. diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 49e7bd1d777432..8794c389d72bcb 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/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 00000000000000..e12bad468009af 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 00000000000000..95fe1c6aa09586 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 00000000000000..5f555f851cd816 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 00000000000000..780a2119037b1a --- /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/ml/index.asciidoc b/docs/user/ml/index.asciidoc index e9ef4a55b2b3ae..6483ddde073350 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -20,7 +20,7 @@ image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight 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-viz} with file upload: diff --git a/package.json b/package.json index 956ad9ee4f71d5..0ad304fdf2f690 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,7 +122,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "18.4.1", + "@elastic/charts": "18.4.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "22.3.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-dev-utils/package.json b/packages/kbn-dev-utils/package.json index ee9f349f490517..08b2e5b226967e 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 00000000000000..6133f9871699f9 --- /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 00000000000000..5fe1844a85563c --- /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 00000000000000..3487de08f034d2 --- /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 305e29a0e41df6..40ee72d94729fd 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 c0bb408d60d8f8..e46075eff63a77 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( @@ -81,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, @@ -95,14 +101,24 @@ run( 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: ['core', 'watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], - string: ['workers', 'scan-dir'], + string: ['workers', 'scan-dir', 'report-stats'], default: { core: true, examples: true, @@ -120,6 +136,7 @@ run( --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/index.ts b/packages/kbn-optimizer/src/index.ts index 8026cf39db73dc..29922944e88172 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/optimizer/handle_optimizer_completion.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts index 5dd500cd7a9e4f..b4b02649259a29 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/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts new file mode 100644 index 00000000000000..375978b9b79447 --- /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 7411e2df1b613c..cc3fa8c2720ded 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -37,7 +37,7 @@ const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset' 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' }, @@ -60,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`) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6a2d02ee778dda..28cf36dedba3f3 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 572f2adb19bd99..7d939e715d411c 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-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 46f55da87575db..a60e2b0449d958 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.4.1", + "@elastic/charts": "18.4.2", "@elastic/eui": "22.3.0", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d3..c0ddcaf4f23c8f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -846,6 +846,14 @@ '@types/semver', ], }, + { + groupSlug: 'set-value', + groupName: 'set-value related packages', + packageNames: [ + 'set-value', + '@types/set-value', + ], + }, { groupSlug: 'sinon', groupName: 'sinon related packages', diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index a87e2aa11f2c0d..32a23d74dbda44 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -258,7 +258,7 @@ export class ClusterManager { ); const ignorePaths = [ - /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, + /[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/, /\.test\.(js|ts)$/, ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), diff --git a/src/core/public/plugins/plugin.test.mocks.ts b/src/core/public/plugins/plugin.test.mocks.ts index b877847aaa90ed..422442c9ca4d2a 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 39330711f79809..8fe745db9554df 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 e51c45040c452e..591165fcd28390 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 abdd9d4ddce2ab..00000000000000 --- 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 b4e2c3095f14a7..00000000000000 --- 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 bf7711055e97ba..00000000000000 --- 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 00000000000000..d4324f81de8e64 --- /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 00000000000000..1907dfa6a3e99c --- /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 a76078932518f2..85b84e561056ad 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 688eaf4f2bfc7a..6d71844bc19c84 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 e698af689036d7..862aa5043ad4b7 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/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index d26cadecb184a8..7958a4f8134d30 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -216,17 +216,7 @@ export class SavedObjectsClient { }), }); - return createRequest - .then(resp => this.createSavedObject(resp)) - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - window.location.assign( - this.http.basePath.prepend('/app/kibana#/error/action.auto_create_index') - ); - } - - throw error; - }); + return createRequest.then(resp => this.createSavedObject(resp)); }; /** @@ -468,9 +458,3 @@ const renameKeys = , U extends Record ...{ [keysMap[key] || key]: obj[key] }, }; }, {}); - -const isAutoCreateIndexError = (error: any) => { - return ( - error?.res?.status === 503 && error?.body?.attributes?.code === 'ES_AUTO_CREATE_INDEX_ERROR' - ); -}; 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 1790b096a71aea..dfa2396d5904bc 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 f9c3dcbf554cb8..df86f5a2b40317 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 2451b98ffdf29d..c707fa2b479e46 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'; @@ -100,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 b38259a84cb9cc..73e119a5a97e73 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 47fc29f8cf7d29..c1b65763949bbe 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 64270c677ff206..3ec478e3ca28db 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 0284f513a361c6..4ddb2b070d3ac3 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/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 7ea61f67e9496b..a33e16895078ed 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -45,10 +45,7 @@ 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; @@ -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; -/** @public */ +/** + * 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 4a43835d795d1d..324d19e2792121 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 478c6b6d26d538..9614d692741e08 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 5f17c117927638..bc8ad2cdb00582 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 b50c6dc9a1abf0..43b76634917114 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 6369720ada2c35..dc1c9d379d508a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1707,7 +1707,7 @@ 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) +// @public export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) @@ -1840,6 +1840,8 @@ export interface SavedObjectsCoreFieldMapping { // (undocumented) index?: boolean; // (undocumented) + null_value?: number | boolean | string; + // (undocumented) type: string; } @@ -1875,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; @@ -1903,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; diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.js b/src/dev/build/tasks/build_kibana_platform_plugins.js index 79cd698e4782c8..28d6b49f9e89a8 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', @@ -32,8 +38,14 @@ export const BuildKibanaPlatformPluginsTask = { 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/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index eb7a121c2e64b2..d1fb544de733c3 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 @@ -234,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/jest/config.js b/src/dev/jest/config.js index 43a2cbd78c5023..c5387590fcf661 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -40,6 +40,7 @@ export default { ], collectCoverageFrom: [ 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index fc95288eabed85..66296736b3ad04 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -117,7 +117,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', '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', diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index ba3cd1280f34bb..6d5fa04a939514 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/es_archiver/actions/rebuild_all.ts b/src/es_archiver/actions/rebuild_all.ts index 1467a1d0430b76..f35b2ca49c666f 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/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8465d71e1e9989..48d86e3628e491 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -118,22 +118,6 @@ export default function(kibana) { }, ], - savedObjectsManagement: { - url: { - defaultSearchField: 'url', - isImportableAndExportable: true, - getTitle(obj) { - return `/goto/${encodeURIComponent(obj.id)}`; - }, - }, - }, - - savedObjectSchemas: { - 'kql-telemetry': { - isNamespaceAgnostic: true, - }, - }, - injectDefaultVars(server, options) { const mapConfig = server.config().get('map'); const tilemap = mapConfig.tilemap; @@ -158,61 +142,6 @@ export default function(kibana) { uiSettingDefaults: getUiSettingDefaults(), }, - 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) { try { // Create the data directory (recursively, if the a parent dir doesn't exist). diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index febdf2cc3d649a..e2cbd584dbe1f4 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,41 +1,9 @@ { - "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" - } - } } } 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 index 21b7ea7dbf4c3b..9f5f4b764f9b05 100644 --- 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 @@ -59,12 +59,14 @@ import { 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; @@ -77,6 +79,18 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let vegaVisType; + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: { + get: () => {}, + }, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; + setKibanaMapFactory(getKibanaMapFactoryProvider(coreSetupMock)); setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, 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 05cea7addf560d..8a542fec0639c3 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 b65571becd83c6..81fef155daf57e 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 a5d8eb80419a11..eb4e109690c37f 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 00000000000000..27f7f4ed3e0732 --- /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 f72794e27e834a..505b0a04c61837 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 4852f71d8c45b3..67f29ee96a3364 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 c3f5859eb454cc..eb529c380cdda4 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 9653f9abab6fb8..4c5e3db3162430 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 2216294fcbac11..5cbd5948bc477a 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 fe25734fcbfde5..d8ce8f1f5f44bf 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 00000000000000..765b9118e68445 --- /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 1269fe7bcf62e9..691417e968eedb 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 caafb2c636271a..506ad2af85c34d 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/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 6ccbc13aeeb571..63664661036520 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/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index d49c59970f521d..26805554370b96 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/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 index 4839870f0f3c8a..564f115cf2c487 100644 --- 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 @@ -23,6 +23,7 @@ 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'; @@ -37,7 +38,8 @@ interface CreateEditFieldProps extends RouteComponentProps { services: { notifications: NotificationsStart; docTitle: ChromeDocTitle; - http: Function; + getHttpStart: () => HttpStart; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; }; } @@ -68,16 +70,14 @@ export const CreateEditField = withRouter( const url = `/management/kibana/index_patterns/${indexPattern.id}`; - if (mode === 'edit') { - if (!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); - } + 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; @@ -88,24 +88,29 @@ export const CreateEditField = withRouter( history.push(`${url}?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`); }; - return ( - - - - - - - - - ); + if (field) { + return ( + + + + + + + + + ); + } else { + return <>; + } } ); 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 e05aea3678fe20..e2f387c0291a7a 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 @@ -95,7 +95,7 @@ uiModules // 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, $http, fieldFormatEditors) => { +const renderCreateEditField = ($scope, $route, getConfig, fieldFormatEditors) => { $scope.$$postDigest(() => { const node = document.getElementById(REACT_FIELD_EDITOR_ID); if (!node) { @@ -112,9 +112,10 @@ const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEdit fieldFormatEditors={fieldFormatEditors} getConfig={getConfig} services={{ - http: $http, + getHttpStart: () => npStart.core.http, notifications: npStart.core.notifications, docTitle: npStart.core.chrome.docTitle, + docLinksScriptedFields: npStart.core.docLinks.links.scriptedFields, }} /> @@ -162,11 +163,11 @@ uiRoutes }, }, controllerAs: 'fieldSettings', - controller: function FieldEditorPageController($scope, $route, $http, Private, config) { + controller: function FieldEditorPageController($scope, $route, Private, config) { const getConfig = (...args) => config.get(...args); const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); + renderCreateEditField($scope, $route, getConfig, fieldFormatEditors); $scope.$on('$destroy', () => { destroyCreateEditField(); 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 6e1b0b71609417..87592cf4e750e4 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 31a27c4da7fcfe..5604067433f136 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 b0cc767a044e88..4bbd839331e56b 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 1453c2155e2d6f..08a73517dc13b3 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 9174b03cf843cf..b7ed14ed3706eb 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 f08d53ee35c8d3..5dbc1ecad277f1 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 cfc7b62ff4f86d..00000000000000 --- 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 3a7615e83f2817..00000000000000 --- 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 0d57120c80a983..00000000000000 --- 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 6df08aea0baa69..8d38095ac25e07 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 8585bf720e0cf7..00000000000000 --- 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 24c721da1f31ac..b4e0dcd5f35102 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 e1966a9e8b2667..3348096c0e2f1e 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(); 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 00000000000000..8020cef8d84927 --- /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/tile_map/common/origin.ts b/src/legacy/core_plugins/tile_map/common/origin.ts deleted file mode 100644 index 7fcf1c659bdf3c..00000000000000 --- 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 3904c43707906b..bce2e157ebbc8d 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 fc029d6bccb6e6..bdf9cd806eb8b5 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 9ca42fe3e4074c..1efb0b2f884f85 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 9df5b94a21edad..00000000000000 --- 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 eec90e512b4623..00000000000000 --- 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 b9acf1a15208f7..f0261483d302d2 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 741e118750f320..dd8d4c6e9311e6 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 2b97407b17b38c..aa1460a7e2890a 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 cfc7b62ff4f86d..00000000000000 --- 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 5296e98b09efe8..00000000000000 --- 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 ae3a839b600e94..ca6a586d220080 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 fdce8bc51fe869..6a7bda5e188831 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 00000000000000..1c87d4dcca2b57 --- /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 e1b4c27319123a..00000000000000 --- 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 7f5c7d4664af86..80ffa440e7285a 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_vislib/index.ts b/src/legacy/core_plugins/vis_type_vislib/index.ts deleted file mode 100644 index da9476285a9b2e..00000000000000 --- 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 e30a9e2b358342..00000000000000 --- 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 3fca9dc8adc08e..00000000000000 --- 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 dc207ad89286f2..00000000000000 --- 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 3d1629740df2c8..00000000000000 --- 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/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/index.ts deleted file mode 100644 index 4d7091ffb204be..00000000000000 --- 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 579caa1cb88f67..00000000000000 --- 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 c04ffa506eb04d..00000000000000 --- 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 26800f8a1620ed..00000000000000 --- 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(data.actions); - } -} 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 0d6b1b5e8de589..00000000000000 --- 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 25c6ae5439fe85..00000000000000 --- 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/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 a3aabcb90be624..00000000000000 --- 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 db99b881a6e380..00000000000000 --- 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 f1c80c99810204..00000000000000 --- 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 734c6d003278fd..00000000000000 --- 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 bc4a4f9925513c..00000000000000 --- 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 fd25335dd2cd4c..00000000000000 --- 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 d4ec6f363a75b0..00000000000000 --- 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 8fe9ac24db77b5..00000000000000 --- 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 4523e70ccbb4cb..00000000000000 --- 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 b5b14c279b40e0..00000000000000 --- 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 98609d8ffbcd3b..00000000000000 --- 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 4ca631c7fc4972..00000000000000 --- 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 13e2ab7b7fb1ac..00000000000000 --- 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 6b7c574ab55511..00000000000000 --- 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 ff5cd05b2f2d49..00000000000000 --- 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 06d9b31dc6b578..00000000000000 --- 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 5208c7e996cd87..00000000000000 --- 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 041fad2ed15b9c..00000000000000 --- 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 cc4f598c7b1b7f..00000000000000 --- 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 2a2d14d59b67ea..00000000000000 --- 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 d283d793151772..00000000000000 --- 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 4e65502f8d2788..00000000000000 --- 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 64deea0e391a6e..00000000000000 --- 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 96d2cfd174579c..00000000000000 --- 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 27050030ebdfd5..00000000000000 --- 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 5c7554db2060df..00000000000000 --- 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 c47155840cec50..00000000000000 --- 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 b05e258133963b..00000000000000 --- 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 d6f3fc4361f329..00000000000000 --- 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 b420565b1c96b5..00000000000000 --- 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 2ac35efadc8f2f..00000000000000 --- 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 5b1e29ac0d54ae..00000000000000 --- 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 147eb691eb67bc..00000000000000 --- 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 3691d854c6c2a9..00000000000000 --- 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 228a22ed534d59..00000000000000 --- 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 4683640725f2a8..00000000000000 --- 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 2b4ee83eca44c7..00000000000000 --- 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 f717012d430cf3..00000000000000 --- 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 2b86663ab4673e..00000000000000 --- 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 8e25015c101860..00000000000000 --- 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 cc6d33a2d98daf..00000000000000 --- 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 3942aa18891b84..00000000000000 --- 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 8978f80f58dde8..00000000000000 --- 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 e9c2ff0d2fa078..00000000000000 --- 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 03646d08298dd6..00000000000000 --- 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 7dfd2ded36a667..00000000000000 --- 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 d42562a87b825a..00000000000000 --- 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 f73011d6616451..00000000000000 --- 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 f4c952be191deb..00000000000000 --- 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 d69f952325ed02..00000000000000 --- 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 c8f0faf8dcca5d..00000000000000 --- 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/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 129006cdf0ca37..00000000000000 --- 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 b2559c085aeab3..00000000000000 --- 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 f33ce0395af1fd..00000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js +++ /dev/null @@ -1,256 +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(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/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 38a6be548594f3..00000000000000 --- 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 024dee60ef2bf4..00000000000000 --- 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 9822932c6071b8..00000000000000 --- 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/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 0d2f3528c90190..02491ff8729812 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 b36e62297cc235..f10718ba58c2cb 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -10,7 +10,6 @@ @import './accessibility/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 92b1bf53520ce3..99f92587507c92 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/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 ad31aabfc66cd2..00000000000000 --- 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 42e672ab322dc1..00000000000000 --- 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 2af31dda6c3457..00000000000000 --- 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 a8f6318090b1d0..00000000000000 --- 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 1ac30b85c5a854..00000000000000 --- 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 09c6bfd93148f9..00000000000000 --- 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 d290e0334b3d6f..00000000000000 --- 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 19d12f4bbbd4c4..00000000000000 --- 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 00000000000000..dc11bdfefa46bf --- /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 b83ae8a901ecc4..00000000000000 --- 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 00000000000000..82d21eb5d30ada --- /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 1f77660c9784cb..00000000000000 --- 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 00000000000000..bf1682faf9a9d9 --- /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 cb5d0758efcdba..00000000000000 --- 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 fea42dee6b6900..00000000000000 --- 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 00000000000000..40443ec2621823 --- /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 00000000000000..aa0f9ebb6567ae --- /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 7e49e93e4cc4fb..00000000000000 --- 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 00000000000000..e93c3c0661c168 --- /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 4ad04f08915e70..00000000000000 --- 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 486b1e34dcade9..00000000000000 --- 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 00000000000000..549831e9c3fb29 --- /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 00000000000000..1fdc36d4e85498 --- /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 e33f0d6ee9c611..00000000000000 --- 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 00000000000000..2d73f775e316c7 --- /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 69cdb159dd4aa9..00000000000000 --- 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 3aa60aa29fcb8f..00000000000000 --- 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 00000000000000..746cb7c7fe3022 --- /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 00000000000000..8bbb379f5df5d6 --- /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 cb570144fcee3f..00000000000000 --- 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 00000000000000..1456deaa13e47b --- /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 0660d06dda9df8..00000000000000 --- 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 32cc55c304e5bf..00000000000000 --- 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 00000000000000..e6b15c741af4ef --- /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 00000000000000..bfce3c973a1fa0 --- /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 0f9eef6f922c84..00000000000000 --- 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 00000000000000..74468ea1451f44 --- /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 6c4cfc9eb21066..00000000000000 --- 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 cf87d4a886024b..00000000000000 --- 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 00000000000000..3f30af97dc063b --- /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 00000000000000..8a98e579665587 --- /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 506002df4fd075..00000000000000 --- 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 00000000000000..a6575f296864d4 --- /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 ef11d70926ad75..00000000000000 --- 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 00000000000000..dbebd324b16b69 --- /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 2b2ae7c8fabb8c..00000000000000 --- 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 8ee130655766f6..00000000000000 --- 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 00000000000000..3ab69d12d8c0e0 --- /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 00000000000000..aed3264bad440a --- /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 f0c331d808a007..00000000000000 --- 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 00000000000000..cf04dd19428e5b --- /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 509508590780fd..00000000000000 --- 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 9d97feaa75a6a8..00000000000000 --- 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 00000000000000..c07c8663593051 --- /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 00000000000000..9279eef7aedebf --- /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 30d1de270522ed..00000000000000 --- 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 00000000000000..0784a3f5e407db --- /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 26effb8d800956..00000000000000 --- 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 853e155814ba62..00000000000000 --- 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 00000000000000..ddeb79538cda1b --- /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 00000000000000..050c7a8a6cf0a2 --- /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 2bfb0bbd15013b..00000000000000 --- 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 00000000000000..2e3c801881f511 --- /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 31ff99696da017..00000000000000 --- 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 c9d153e1b32b0f..00000000000000 --- 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 00000000000000..2e2b1c3ae2357d --- /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 00000000000000..f998e271b6c999 --- /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 270ff844fd0863..00000000000000 --- 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 00000000000000..cde081ff10d147 --- /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 d3de10e3e532ce..00000000000000 --- 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 80334e3801e8bf..00000000000000 --- 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 00000000000000..d0fa0935b26640 --- /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 00000000000000..f13fe44ee4280c --- /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 729487dfae5d79..00000000000000 --- 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 00000000000000..f646d5b4afca80 --- /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 5009ba5844eda4..00000000000000 --- 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 00000000000000..3d7bba0fce9077 --- /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 9a9b6c954b78d0..00000000000000 --- 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 7ab6f2a9cbeb0d..00000000000000 --- 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 00000000000000..bb723386ff7773 --- /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 00000000000000..9fd44c5f9655de --- /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 c727f54874db4d..00000000000000 --- 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 00000000000000..a3418077ad2586 --- /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 3f91f6f4253adb..00000000000000 --- 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 00000000000000..1ce7bec579e164 --- /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 d4a4293b456698..00000000000000 --- 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 1d732b50db3d00..00000000000000 --- 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 00000000000000..4d09da84edfb63 --- /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 00000000000000..73a130d442eb0f --- /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 204554ad94644c..00000000000000 --- 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 9e038413978723..00000000000000 --- 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 00000000000000..f6e631c8b7ac05 --- /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 00000000000000..2de6dff5d251a3 --- /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 73a7c1141c6014..00000000000000 --- 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 00000000000000..2883ffb6bc8a13 --- /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 b3345f085882c4..00000000000000 --- 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 8e18f1f5f4de8b..00000000000000 --- 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 00000000000000..01f405e9aff1f0 --- /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 00000000000000..d63674bf4d2053 --- /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 f09ea05f6711a2..00000000000000 --- 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 00000000000000..b331c1e38eb349 --- /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 b810541d726977..00000000000000 --- 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 48094f63f8fe82..00000000000000 --- 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 00000000000000..8568c2c79816b5 --- /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 00000000000000..7dac6681fa1ea8 --- /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 ca252b6d0147b6..00000000000000 --- 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 00000000000000..282e8e311d984d --- /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 c512b5f5f2019c..00000000000000 --- 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 2fac8c7641ddb8..00000000000000 --- 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 00000000000000..4106eb7b283eea --- /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 00000000000000..6f51379c796d49 --- /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 ba47b94aaea0c5..00000000000000 --- 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 00000000000000..8158c6881acf9e --- /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 12bf5c1cce0043..00000000000000 --- 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 00000000000000..8d3a83a155553c --- /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 e90cb110ac3048..00000000000000 --- 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 cf61b6140f42cc..00000000000000 --- 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 00000000000000..5716305b514830 --- /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 00000000000000..aa62a53f2c32a1 --- /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 47e2091565c30b..00000000000000 --- 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 00000000000000..4db827a4229fd8 --- /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 00000000000000..174cb7da73cebe --- /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 c7a778ac18bd37..be8ab4cf8d6962 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 c918554563fcb7..6f1120dce0c7cf 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 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 85850e56fdb865..00000000000000 --- 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 00000000000000..5489ce9250e043 --- /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 283a3273a2a5df..459e72c0c67c16 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_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 8a71c6ccb15064..b60442690af3c3 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -29,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'; @@ -36,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/ --}} @@ -46,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) { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 801eecf5b608bd..9b44395fa9c68a 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,9 +96,12 @@ 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` ); @@ -106,15 +111,15 @@ export function uiRenderMixin(kbnServer, server, config) { const styleSheetPaths = [ ...(isCore ? [] : dllStyleChunks), - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `${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`, ]), @@ -129,13 +134,23 @@ export function uiRenderMixin(kbnServer, server, config) { ) .map(path => path.localPath.endsWith('.scss') - ? `${basePath}/built_assets/css/${path.publicPath}` + ? `${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}` @@ -148,20 +163,22 @@ export function uiRenderMixin(kbnServer, server, config) { ...dllJsChunks, `${regularBundlePath}/commons.bundle.js`, ]), - `${regularBundlePath}/plugin/kibanaUtils/kibanaUtils.plugin.js`, - `${regularBundlePath}/plugin/esUiShared/esUiShared.plugin.js`, - `${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`, - ]; - const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()]; + ...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.js + // src/optimize/bundles_route/bundles_route.ts const publicPathMap = JSON.stringify({ core: `${regularBundlePath}/core/`, 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, - ...uiPluginIds.reduce( - (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }), + ...kpPluginIds.reduce( + (acc, pluginId) => ({ + ...acc, + [pluginId]: `${regularBundlePath}/plugin/${pluginId}/`, + }), {} ), }); diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 0b2aeda11fb0e4..902fa59b205698 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 4030988c8552c7..00000000000000 --- a/src/optimize/bundles_route/bundles_route.js +++ /dev/null @@ -1,160 +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, join } 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'; -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 = [], -}) { - // 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({ - publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - routePath: '/bundles/kbn-ui-shared-deps/', - bundlesPath: UiSharedDeps.distDir, - fileHashCache, - replacePublicPath: false, - }), - ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/plugin/${id}/`, - routePath: `/bundles/plugin/${id}/`, - bundlesPath: path, - fileHashCache, - replacePublicPath: false, - }) - ), - buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/core/`, - routePath: `/bundles/core/`, - bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), - fileHashCache, - replacePublicPath: false, - }), - buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/`, - routePath: '/bundles/', - bundlesPath: regularBundlesPath, - fileHashCache, - }), - buildRouteForBundles({ - publicPath: `${basePublicPath}/built_assets/dlls/`, - routePath: '/built_assets/dlls/', - bundlesPath: dllBundlesPath, - fileHashCache, - }), - buildRouteForBundles({ - publicPath: `${basePublicPath}/`, - routePath: '/built_assets/css/', - bundlesPath: builtCssPath, - fileHashCache, - }), - ]; -} - -function buildRouteForBundles({ - publicPath, - routePath, - bundlesPath, - fileHashCache, - replacePublicPath = true, -}) { - 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, - replacePublicPath, - }); - }, - }, - }, - }, - 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 00000000000000..e9cfba0130d951 --- /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 80c49a26270fd6..00000000000000 --- a/src/optimize/bundles_route/dynamic_asset_response.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 { 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, replacePublicPath } = 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 - - const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; - const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; - - return h - .response(content) - .takeover() - .code(200) - .etag(etag) - .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 00000000000000..a020c6935eeecf --- /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 d9464cf05eca17..00000000000000 --- 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 00000000000000..7b0801098ed10b --- /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 00000000000000..a7cdabbff13a7b --- /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 fff0ec444d95ba..00000000000000 --- 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 00000000000000..1d189054324a1c --- /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 b7b9f7712358a6..363f81a6a3a967 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 de05fd2b863b88..00000000000000 --- 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 00000000000000..e7c3207948f6a3 --- /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 00000000000000..9a3f08e2f667eb --- /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 ef05d9e5ae704e..00000000000000 --- 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 00000000000000..1ec2b4a431aa62 --- /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 a31ef7229e5da3..1f6107996277cb 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 6093658ae1a2d8..0f6f3b2d4b6226 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 6c20f21c7768e9..cdff57a00c2e09 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 74a96dc8aea6e6..81e04a5b83956a 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 cac9a6daa8df85..e6ca6e797ba450 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 00000000000000..083d5f3ffced4f --- /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 00000000000000..ffcf7cd49a8c32 --- /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 00000000000000..4d7bd34259819d --- /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/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index a1696298117b0b..37f014d8360751 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, 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 fb33649093c8d9..f8632011002d0e 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -275,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/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index 836cea298f0354..5dab21ff671b42 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/server/capabilities_provider.ts b/src/plugins/dashboard/server/capabilities_provider.ts new file mode 100644 index 00000000000000..0bb53d60c38a51 --- /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/plugin.ts b/src/plugins/dashboard/server/plugin.ts index 5d1b66002e7491..ba7bdeeda01335 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -26,6 +26,7 @@ import { } from '../../../core/server'; import { dashboardSavedObjectType } from './saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; @@ -40,6 +41,7 @@ export class DashboardPlugin implements Plugin { const config = this.getDefaultConfig(fieldType, esTypes); diff --git a/src/plugins/data/common/utils/abort_utils.ts b/src/plugins/data/common/utils/abort_utils.ts index 5051515f3a8265..9aec787170840f 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/index.ts b/src/plugins/data/public/index.ts index e1e2576b2a0e73..60d8079b22347e 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 { diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index 0fb92393d56f71..d83c0a7d3445eb 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 d6067280fd7b67..9772370199b24a 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/public.api.md b/src/plugins/data/public/public.api.md index f5177df022ff2e..86560b3ccf7b12 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -451,6 +451,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) @@ -469,6 +560,13 @@ 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) @@ -825,17 +923,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) @@ -912,58 +1010,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) @@ -1788,7 +1843,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 @@ -1804,28 +1858,28 @@ 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:381:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400: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:384:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:415: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 "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts 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 00000000000000..d603830e44a53b --- /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/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 58e8fbae9f9e24..3e31f8e8a566d0 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 e45ad796bd9d46..3dfaa9c6d0a986 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 5d980974474de8..43262001411798 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/kql_telementry.ts b/src/plugins/data/server/saved_objects/kql_telementry.ts new file mode 100644 index 00000000000000..6539d5eacfde21 --- /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/server.api.md b/src/plugins/data/server/server.api.md index 9b673de60ca65a..5d94b6516c2bab 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -609,7 +609,7 @@ export class Plugin implements Plugin_2 { // (undocumented) setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 91d6358d44c18f..2d41293f263698 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/server/capabilities_provider.ts b/src/plugins/discover/server/capabilities_provider.ts new file mode 100644 index 00000000000000..2e03631894aee6 --- /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 00000000000000..15a948c56148ef --- /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 00000000000000..04502f5fc14e60 --- /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/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index d07bf915845e9a..fc5438b8c8dcb8 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 044e7b5d35ad8b..0abbc25ff49a62 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 2a0ffd723850b3..b046376a304ae0 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 4c08a80a356bff..70628665e6e8c9 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 b95060a73252f0..c43359382a33d2 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 a88c3ba0863259..31e14a0af59d7d 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/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 01fbf52c801823..36f49f2508e805 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 3bd414ecf0d4a9..ebb76c743393bc 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 1aae43550ec6fc..d2769e208ba422 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 19e461b8bde7e8..a9cb83504d9584 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 0e03db3ec83584..ef3c4b6f17e7f6 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/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts new file mode 100644 index 00000000000000..ecbf94eceae642 --- /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 d0ab178296408a..6ee12d97a64226 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 4b255a0f967b26..35554954d08282 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 8236ff8709a823..bc27b0eda49598 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/home/server/capabilities_provider.ts b/src/plugins/home/server/capabilities_provider.ts new file mode 100644 index 00000000000000..1c662e2301e16c --- /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 1f530bc58f0b95..1050c19362ae17 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -26,6 +26,7 @@ import { SampleDataRegistryStart, } from './services'; import { UsageCollectionSetup } from '../../usage_collection/server'; +import { capabilitiesProvider } from './capabilities_provider'; import { sampleDataTelemetry } from './saved_objects'; interface HomeServerPluginSetupDependencies { @@ -38,6 +39,7 @@ export class HomeServerPlugin implements Plugin ({ + 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 00000000000000..afc7adf8832e1d --- /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 00000000000000..f8fda7da9b95a1 --- /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 4cbe098501c675..822378163a7ebc 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 00000000000000..be9c4d0d9c37bb --- /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 00000000000000..e6cabdde82cd9a --- /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 00000000000000..0da597068f11e0 --- /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 47f5b8f31e62be..d1def8153d1a84 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 e74c260d3b8e5c..4892463bb9f856 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 1dac4607280cc7..c4ac671a5187c6 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 1c4d0882cb7da4..c7cec1b14159aa 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 f4f0d66ee20ded..8e3a0648e99d4b 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 751be65e1dbf6e..acc7655a5e2636 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 00000000000000..85631408169971 --- /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/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index fe3150fc0bb076..c1daf3445219f9 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,110 +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/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx new file mode 100644 index 00000000000000..5ac6e8e103c47c --- /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 00000000000000..7660d17f91c5bb --- /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/server/capabilities_provider.ts b/src/plugins/saved_objects_management/server/capabilities_provider.ts new file mode 100644 index 00000000000000..bd621de4a61953 --- /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 b72644b500967d..4e39b08f5c62c9 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 bcb681a50652a8..0d9f183d13404b 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 00000000000000..956f031d2f1a54 --- /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 00000000000000..c76c21993a13f1 --- /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_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 00000000000000..8c0117e5a72664 --- /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 00000000000000..d0c2bd13f802d7 --- /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 26e075b666593e..361c5ff719c54e 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/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts index d69b9bba31b032..e8404f918d6092 100644 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/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 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; 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 7075e86eb56bf1..9fdb8ccc919b79 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 00000000000000..d81bd95d8d7710 --- /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/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index af50d3a06d1fc6..f21b5f947bca79 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/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 008b13cce65659..b573225feaab1d 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -20,7 +20,6 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; -import { PersistedState } from '../../visualizations/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; @@ -76,6 +75,7 @@ export const createMetricsFn = (): ExpressionFunctionDefinition< const params = JSON.parse(args.params); const uiStateParams = JSON.parse(args.uiState); const savedObjectId = args.savedObjectId; + const { PersistedState } = await import('../../visualizations/public'); const uiState = new PersistedState(uiStateParams); const response = await metricsRequestHandler({ diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index c525ce7fa0b3bd..2b0734ceb4d4d5 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,6 +25,7 @@ import { EditorController } from './application'; // @ts-ignore import { PANEL_TYPES } from '../common/panel_types'; import { defaultFeedbackMessage } from '../../kibana_utils/public'; +import { VisEditor } from './application/components/vis_editor_lazy'; export const metricsVisDefinition = { name: 'metrics', @@ -69,7 +70,7 @@ export const metricsVisDefinition = { show_legend: 1, show_grid: 1, }, - component: require('./application/components/vis_editor').VisEditor, + component: VisEditor, }, editor: EditorController, options: { 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 3127e03ada0efa..fa4427fbb8c12b 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/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index f18fa1e4cc2fa6..34922976f22ff5 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -41,5 +41,6 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { }, migrations: { '7.7.0': flow(resetCount), + '7.8.0': flow(resetCount), }, }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index c312705194cde2..b52dcfbd914f9a 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -26,14 +26,14 @@ import { setSavedObjects, setInjectedVars, setUISettings, + setKibanaMapFactory, } from './services'; import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { IServiceSettings } from '../../maps_legacy/public'; -import { ConfigSchema } from '../config'; - +import { getKibanaMapFactoryProvider, IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; +import { ConfigSchema } from '../config'; /** @internal */ export interface VegaVisualizationDependencies { @@ -75,6 +75,7 @@ export class VegaPlugin implements Plugin, void> { emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); + setKibanaMapFactory(getKibanaMapFactoryProvider(core)); const visualizationDependencies: Readonly = { core, diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index e349cfbdc0024d..f81f87d7ad2e17 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -27,6 +27,9 @@ export const [getData, setData] = createGetterSetter('Dat export const [getNotifications, setNotifications] = createGetterSetter( 'Notifications' ); +export const [getKibanaMapFactory, setKibanaMapFactory] = createGetterSetter( + 'KibanaMapFactory' +); export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index bd6652a597203b..895d496a896aa3 100644 --- a/src/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 '../../../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/plugins/vis_type_vislib/kibana.json b/src/plugins/vis_type_vislib/kibana.json new file mode 100644 index 00000000000000..5b3088b399ebff --- /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 8a196da64fc4bc..c42962ad50a4b0 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 00000000000000..0bd5694f710213 --- /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 433cc4edeb47b3..c297fb08e4b68b 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 48711de7d171af..b299b2e86ca403 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 00000000000000..7a89496d9441e8 --- /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 00000000000000..8d5f529ce0fc76 --- /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 91cdcd0f456b15..44ed0d5aeddabd 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 246c20a14807c5..468fb1f8c315af 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 c913fd4f35713d..e2d4a0db9f1f9c 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/legacy/core_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 similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx index 89aab3a19c5897..623a8d1f348e93 100644 --- a/src/legacy/core_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 @@ -22,12 +22,12 @@ 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 { Vis } from '../../../../../visualizations/public'; import { SeriesParam, ValueAxis } from '../../../types'; import { ChartTypes } from '../../../utils/collections'; -import { SelectOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption } from '../../../../../charts/public'; import { LineOptions } from './line_options'; -import { SetParamByIndex, ChangeValueAxis } from './'; +import { SetParamByIndex, ChangeValueAxis } from '.'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; 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 a93ee454a7afd1..4798c67928f7f6 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 a3a97df9e35ae2..634d6b3f0641cd 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 48fcbdf8f9082e..f500b7e58e9fdb 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 bc687e56646f63..14e1da6ebcc701 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 5354bc9c033e6e..e90c96146ec2cd 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 76f95bd93caf8c..4b0cce94267f12 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/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts index a296281375daca..277fcf0cdbc3de 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { Axis, ValueAxis, SeriesParam } from '../../../types'; import { ChartTypes, @@ -31,7 +31,7 @@ import { getPositions, getInterpolationModes, } from '../../../utils/collections'; -import { Style } from '../../../../../../../plugins/charts/public'; +import { Style } from '../../../../../charts/public'; const defaultValueAxisId = 'ValueAxis-1'; 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 22a726b53363b0..27c423860972c0 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 141273fa6bc3f8..2f7dd4071b52cd 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 912c3b904b110b..b17f67b81d2b04 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 876a6917ee0b4c..1977bdba6eadfc 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 1b89a766d05913..52962fe813b446 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 b5ed475ca8e319..3bacb0be34d135 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 faeb6069b51264..c2aa917dd3a6f6 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 f6be9cd0bd8fed..54ba307982967e 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 392d180d2c5f26..0126dce37c9f24 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 903c1917751d9e..60458b1f5c41f0 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 12f058ec7dd1fc..08231803007563 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 00000000000000..ff8538021d2756 --- /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 00000000000000..6367197acdecea --- /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 00000000000000..ba0d8bf251c6fa --- /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 00000000000000..89e4f9a32cee16 --- /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 00000000000000..85078a2ec15af2 --- /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 00000000000000..821c04685d22e5 --- /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 00000000000000..65821ac58eb0db --- /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 00000000000000..b6f731c9655d41 --- /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 00000000000000..8144a996e3424b --- /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 00000000000000..e783246972e4a6 --- /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 00000000000000..71ee039f989383 --- /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 00000000000000..c1044160c0e7a6 --- /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 00000000000000..a26dc9bd8b181c --- /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 00000000000000..ca4cb2a7feee1f --- /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 00000000000000..c93365234d1582 --- /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 00000000000000..d88197c3737e52 --- /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 00000000000000..99511e693ff023 --- /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 00000000000000..c23a89b755b5b3 --- /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 00000000000000..df71f4efc58b55 --- /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 00000000000000..b5b931383f7321 --- /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 00000000000000..bc7e4c9f49625f --- /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 00000000000000..40c14beeb4f3ef --- /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 00000000000000..bf4fcb7e9e526a --- /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 00000000000000..5d737131dc9980 --- /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 00000000000000..36df8e091ba895 --- /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 00000000000000..a914f20a7ffc6f --- /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 00000000000000..8891d9badb2be3 --- /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 00000000000000..09a1c159897603 --- /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 00000000000000..c55bff5631e888 --- /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 00000000000000..372325120ee8ec --- /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 00000000000000..60edf6c1ff05cc --- /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 5e0b2b8fbd36cb..561c45d26fa7f6 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 0f70dca69728d2..5f74698938a0b1 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 9feed60b984bae..ced7a38568ffd0 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 54ccf66f362ca3..52242ad11e8f58 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 6f732717266601..a58c15f136431e 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 00000000000000..665643a6763f6b --- /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 1f9a8d77398e63..a94fd3f3945ab7 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 2774836baa3817..a68bc5893406f5 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 15c80e47194872..a8c03eba2b4499 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 452e0be0df3e4a..52da0f7ac14eca 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 00000000000000..e19a2ec451f2b8 --- /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 00000000000000..633fae9c7f2a61 --- /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 00000000000000..83d0b49b1c5518 --- /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 2024c43dd1c8b4..44df4864bfd68c 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/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx rename to src/plugins/vis_type_vislib/public/vis_controller.tsx index ec091e5d29cfdf..65acc08b58da02 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -24,9 +24,9 @@ import React, { RefObject } from 'react'; import { Vis as Vislib } from './vislib/vis'; import { Positions } from './utils/collections'; import { VisTypeVislibDependencies } from './plugin'; -import { mountReactNode } from '../../../../core/public/utils'; +import { mountReactNode } from '../../../core/public/utils'; import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; -import { VisParams, ExprVis } from '../../../../plugins/visualizations/public'; +import { VisParams, ExprVis } from '../../visualizations/public'; const legendClassName = { top: 'visLib--legend-top', 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 854b70b04e58a6..a4243c6d25c417 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 00000000000000..838275d44d2a1a --- /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 98% 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 6bf66c2bdd788b..c203642f353c53 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,10 +31,6 @@ jest.mock('@elastic/eui', () => ({ htmlIdGenerator: jest.fn().mockReturnValue(() => 'legendId'), })); -jest.mock('../../../legacy_imports', () => ({ - getTableAggs: jest.fn(), -})); - jest.mock('../../../services', () => ({ getDataActions: () => ({ createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx rename to src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx 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 d9eea83d40b48c..0167a542c6372e 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 00000000000000..953c115cc0d024 --- /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 00000000000000..c184d0e9fbcf7f --- /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 00000000000000..df502b7cde3dfb --- /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 9014349c38d253..c2965e81657596 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 2c6d62ed084b57..c3b82f72af482e 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 0c79c5b263ceac..dc10c9f4938a0e 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 00000000000000..b7764b92b78059 --- /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 00000000000000..dec7de5ceeda95 --- /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 00000000000000..7901919d306d27 --- /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 00000000000000..d007a8a14de131 --- /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 00000000000000..85378ff1a14e87 --- /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 00000000000000..b1a91979b3d9d8 --- /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 e22f19ea643fd3..4e650d4c20f977 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 00000000000000..a680788281fb16 --- /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 00000000000000..f5b1c13f1a83f7 --- /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 00000000000000..0bc11e5124a077 --- /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 00000000000000..117b346efda898 --- /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 00000000000000..05f6f72246d4a8 --- /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 00000000000000..a27ee57e64a5a9 --- /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 00000000000000..684d8f346744e2 --- /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 00000000000000..1ba7d4aaa8a0c9 --- /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 62de6d84136641..deafe010b6773c 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 00000000000000..6f497ae057d726 --- /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 00000000000000..058bdb5de8785f --- /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 00000000000000..df044f46460c87 --- /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 00000000000000..355c01d255ce77 --- /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/visualize/kibana.json b/src/plugins/visualize/kibana.json index a7afa0697a5eb1..d536d2f246a6b9 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 9d8a1b98ef023d..19551bba9a43ef 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/server/capabilities_provider.ts b/src/plugins/visualize/server/capabilities_provider.ts new file mode 100644 index 00000000000000..3b09b28f53c77e --- /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 00000000000000..5cebef71d8d22e --- /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 00000000000000..7cc57c25f3229e --- /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 1ec7c831b48641..f87edbf04f220c 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() { @@ -57,27 +58,30 @@ module.exports = function(grunt) { '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`, ]; } @@ -127,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/api_integration/apis/index.js b/test/api_integration/apis/index.js index c5bfc847d0041f..0c4028905657d5 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,5 +33,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js new file mode 100644 index 00000000000000..c79f5cb4708903 --- /dev/null +++ b/test/api_integration/apis/telemetry/index.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ loadTestFile }) { + describe('Telemetry', () => { + loadTestFile(require.resolve('./telemetry_local')); + loadTestFile(require.resolve('./opt_in')); + loadTestFile(require.resolve('./telemetry_optin_notice_seen')); + }); +} diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts new file mode 100644 index 00000000000000..e4654ee3985f32 --- /dev/null +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('/api/telemetry/v2/optIn API', () => { + let defaultAttributes: TelemetrySavedObjectAttributes; + let kibanaVersion: any; + before(async () => { + const kibanaVersionAccessor = kibanaServer.version; + kibanaVersion = await kibanaVersionAccessor.get(); + defaultAttributes = + (await getSavedObjectAttributes(supertest).catch(err => { + if (err.message === 'expected 200 "OK", got 404 "Not Found"') { + return null; + } + throw err; + })) || {}; + + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + }); + + afterEach(async () => { + await updateSavedObjectAttributes(supertest, defaultAttributes); + }); + + it('should support sending false with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, false, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(false); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should support sending true with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, true, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(true); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should not support sending false with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, false, 400); + }); + + it('should not support sending true with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, true, 400); + }); + + it('should not support sending null', async () => { + await postTelemetryV2Optin(supertest, null, 400); + }); + + it('should not support sending junk', async () => { + await postTelemetryV2Optin(supertest, 42, 400); + }); + }); +} + +async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise { + const { body } = await supertest + .post('/api/telemetry/v2/optIn') + .set('kbn-xsrf', 'xxx') + .send({ enabled: value }) + .expect(statusCode); + + return body; +} + +async function updateSavedObjectAttributes( + supertest: any, + attributes: TelemetrySavedObjectAttributes +): Promise { + return await supertest + .post('/api/saved_objects/telemetry/telemetry') + .query({ overwrite: true }) + .set('kbn-xsrf', 'xxx') + .send({ attributes }) + .expect(200); +} + +async function getSavedObjectAttributes(supertest: any): Promise { + const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200); + return body.attributes; +} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js new file mode 100644 index 00000000000000..84bfd8a755c116 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; + +/* + * Create a single-level array with strings for all the paths to values in the + * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn + * in the tests. + */ +function flatKeys(source) { + const recursivelyFlatKeys = (obj, path = [], depth = 0) => { + return depth < 3 && _.isObject(obj) + ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1)) + : path.join('.'); + }; + + return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b)); +} + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/clusters/_stats', () => { + it('should pull local stats and validate data types', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.collection).to.be('local'); + expect(stats.stack_stats.kibana.count).to.be.a('number'); + expect(stats.stack_stats.kibana.indices).to.be.a('number'); + expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); + expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); + expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); + expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + }); + + it('should pull local stats and validate fields', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + const stats = body[0]; + + const actual = flatKeys(stats); + expect(actual).to.be.an('array'); + const expected = [ + 'cluster_name', + 'cluster_stats.cluster_uuid', + 'cluster_stats.indices.analysis', + 'cluster_stats.indices.completion', + 'cluster_stats.indices.count', + 'cluster_stats.indices.docs', + 'cluster_stats.indices.fielddata', + 'cluster_stats.indices.mappings', + 'cluster_stats.indices.query_cache', + 'cluster_stats.indices.segments', + 'cluster_stats.indices.shards', + 'cluster_stats.indices.store', + 'cluster_stats.nodes.count', + 'cluster_stats.nodes.discovery_types', + 'cluster_stats.nodes.fs', + 'cluster_stats.nodes.ingest', + 'cluster_stats.nodes.jvm', + 'cluster_stats.nodes.network_types', + 'cluster_stats.nodes.os', + 'cluster_stats.nodes.packaging_types', + 'cluster_stats.nodes.plugins', + 'cluster_stats.nodes.process', + 'cluster_stats.nodes.versions', + 'cluster_stats.status', + 'cluster_stats.timestamp', + 'cluster_uuid', + 'collection', + 'collectionSource', + 'stack_stats.kibana.count', + 'stack_stats.kibana.indices', + 'stack_stats.kibana.os', + 'stack_stats.kibana.plugins', + 'stack_stats.kibana.versions', + 'timestamp', + 'version', + ]; + + expect(expected.every(m => actual.includes(m))).to.be.ok(); + }); + }); +} diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts new file mode 100644 index 00000000000000..4413c672fb46c3 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch'; +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const client: Client = getService('legacyEs'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + it('should update telemetry setting field via PUT', async () => { + try { + await client.delete({ + index: '.kibana', + id: 'telemetry:telemetry', + } as DeleteDocumentParams); + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + + await supertest + .put('/api/telemetry/v2/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { + _source: { telemetry }, + }: GetResponse<{ + telemetry: TelemetrySavedObjectAttributes; + }> = await client.get({ + index: '.kibana', + id: 'telemetry:telemetry', + } as GetParams); + + expect(telemetry.userHasSeenNotice).to.be(true); + }); + }); +} diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 456752c0cd6ebd..47b8a4a2e9f709 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 a56a85546bbcd8..9a0130d39bc2fe 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 f388993dcaf7dc..8846a753f37948 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 2ea1389b89ad43..7ffe951faa3988 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/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 6e24b9f3570a3b..279adb22a1cfa4 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 b7698a7d6ac4be..564eb790eb8d19 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/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 08e0cb0b8d23a7..ebce8dcafadb4c 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 62d42f3da5c846..ace9710665f10a 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/getting_started/index.js b/test/functional/apps/getting_started/index.js index b73c16e2583b5f..41ee71a753712f 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 6587c2f113b7f3..3c56c22c046dd6 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 5812b9b96e42a1..f46b3390cbcf8c 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 c8d11d8555d37f..65d852b249ea0a 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/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index c45a95abab86ee..22d36d671cb683 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 7ebb4548f967ba..d7a30f39250f3d 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 cbf7ab8df6831e..842599fa1d0a12 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 d989f8e2539a00..256c0362226e52 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 867db66ac81dcd..27a06cc05b45c3 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/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index b56a37218aba56..b5231f3161377b 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 862e5127bb6704..93debdcc37f0ab 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl: boolean; shouldLoginIfPrompted: boolean; useActualUrl: boolean; + insertTimestamp: boolean; } class CommonPage { @@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string) { + private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting @@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout ); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); currentUrl = await browser.getCurrentUrl(); log.debug(`Finished login process currentUrl = ${currentUrl}`); } @@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } private async navigate(navigateProps: NavigateProps) { - const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps; + const { + appConfig, + ensureCurrentUrl, + shouldLoginIfPrompted, + useActualUrl, + insertTimestamp, + } = navigateProps; const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); await retry.try(async () => { @@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = false, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = true, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (config.has(['apps', appName])) { @@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo log.debug('returned from get, calling refresh'); await browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 0129d4f1bce9f8..2bba6e06d76d7e 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 67d88b308ed91c..951ba8e22d8858 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index 5fe8b41cc00102..fdaee76cafa9de 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 1c55c676d94253..b4e4a115f20111 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 a8e50e017102f0..4acb170d125744 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -36,14 +36,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 af5ace8e3cd3b7..4f1251321b0054 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -34,6 +34,20 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, '^(!!)?file-loader!': fileMockPath, }, + collectCoverageFrom: [ + 'legacy/plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/server/**/*.{js,jsx,ts,tsx}', + 'plugins/**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/flot-charts/**', + '!**/test/**', + '!**/build/**', + '!**/scripts/**', + '!**/mocks/**', + '!**/plugins/apm/e2e/**', + ], + coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ diff --git a/x-pack/index.js b/x-pack/index.js index 82a35f1710361a..cfadddac3994ac 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,21 +12,12 @@ import { dashboardMode } from './legacy/plugins/dashboard_mode'; 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 { 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 [ @@ -40,18 +31,9 @@ module.exports = function(kibana) { apm(kibana), maps(kibana), canvas(kibana), - indexManagement(kibana), infra(kibana), taskManager(kibana), - rollup(kibana), - siem(kibana), - remoteClusters(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 276d1ea3accea6..00000000000000 --- 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 63dd6f99f9c24b..00000000000000 --- a/x-pack/legacy/plugins/actions/server/index.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 { Root } from 'joi'; -import { Legacy } from 'kibana'; -import mappings from './mappings.json'; -import { - LegacyPluginApi, - LegacyPluginSpec, - ArrayOrItem, -} from '../../../../../src/legacy/plugin_discovery/types'; - -export function actions(kibana: LegacyPluginApi): ArrayOrItem { - 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, - }, - } as Legacy.PluginSpecOptions); -} diff --git a/x-pack/legacy/plugins/alerting/index.ts b/x-pack/legacy/plugins/alerting/index.ts deleted file mode 100644 index 0d0a698841269f..00000000000000 --- 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 065af7dedebd92..00000000000000 --- a/x-pack/legacy/plugins/alerting/server/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 { Legacy } from 'kibana'; -import { Root } from 'joi'; -import mappings from './mappings.json'; -import { - LegacyPluginApi, - LegacyPluginSpec, - ArrayOrItem, -} from '../../../../../src/legacy/plugin_discovery/types'; - -export function alerting(kibana: LegacyPluginApi): ArrayOrItem { - 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, - }, - } as Legacy.PluginSpecOptions); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index eb71994bd2e472..e9942a327b69ed 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -60,7 +60,10 @@ const style: cytoscape.Stylesheet[] = [ ? theme.euiColorPrimary : theme.euiColorMediumShade, 'border-width': 2, - color: theme.textColors.text, + 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/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 index 27095264461ee3..f57ddb5cf69a23 100644 --- 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 @@ -71,10 +71,10 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = styled(EuiBadge)` +const SpanBadge = (styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as any; +` as unknown) as typeof EuiBadge; const HttpInfoContainer = styled('div')` margin-right: ${px(units.quarter)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index a5d8902ff16261..f01b2aa335a3aa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/legacy/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 any; +` as unknown) as typeof EuiBadge; interface SyncBadgeProps { /** diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 858e6d29bfa5e3..2be3c82a8385b0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -17,7 +17,7 @@ interface Props { const Badge = (styled(EuiBadge)` margin-top: ${px(units.eighth)}; -` as any) as any; +` as unknown) as typeof EuiBadge; export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( 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 index 1e1d49b2cf417c..d499dddeeb8b3f 100644 --- 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 @@ -13,7 +13,7 @@ import { HttpStatusBadge } from '../HttpStatusBadge'; const HttpInfoBadge = (styled(EuiBadge)` margin-right: ${px(units.quarter)}; -` as any) as any; +` as unknown) as typeof EuiBadge; const Url = styled('span')` display: inline-block; 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 ee08dfb87e1c13..1e8983a0ca5e52 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 @@ -45,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/public/components/app/track_route_change.js b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js index e837f5200a1590..2886aa868eb9e1 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/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index a416adfe774697..5c420cf3a04c9c 100644 --- 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 @@ -93,8 +93,8 @@ export const ElementMenu: FunctionComponent = ({ const hideAssetModal = () => setAssetModalVisible(false); const showAssetModal = () => setAssetModalVisible(true); - const showEmbedPanel = () => setEmbedPanelVisible(false); const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); const hideSavedElementsModal = () => setSavedElementsModalVisible(false); const showSavedElementsModal = () => setSavedElementsModalVisible(true); diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 5bb628909c32e2..f83887bbcbdfd3 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -35,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 834d5868c35ea7..57b513affd7815 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 4118bb81b83639..8952802dc2f2be 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 @@ -8,10 +8,10 @@ import { AxiosPromise } from 'axios'; import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; 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}`; }; 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 40e09ee39d7144..6430f7d87d4f76 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/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 6aa4968f29155d..184f4f3c8af7c3 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -11,21 +11,21 @@ import { API_ROUTE } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; import { notifyService } from '../services'; -import { getCoreStart } from '../legacy'; +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') => { 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 f3681f50c56a59..e6628399f53c22 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 36ce1974be2724..baeb4ebd453d2c 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -44,7 +44,6 @@ export interface CanvasStartDeps { uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; - formatMsg: any; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; } @@ -64,6 +63,7 @@ export class CanvasPlugin implements Plugin { // 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, plugins: CanvasSetupDeps) { const { api: canvasApi, registries } = getPluginApi(plugins.expressions); @@ -73,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); @@ -115,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 index 12c0a687bf308b..17d836f1441c96 100644 --- a/x-pack/legacy/plugins/canvas/public/services/index.ts +++ b/x-pack/legacy/plugins/canvas/public/services/index.ts @@ -7,6 +7,7 @@ import { CoreSetup, CoreStart } from '../../../../../../src/core/public'; import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; import { notifyServiceFactory } from './notify'; +import { platformServiceFactory } from './platform'; export type CanvasServiceFactory = ( coreSetup: CoreSetup, @@ -49,6 +50,7 @@ export type ServiceFromProvider

= P extends CanvasServiceProvider ? export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), + platform: new CanvasServiceProvider(platformServiceFactory), }; export interface CanvasServices { @@ -70,4 +72,4 @@ export const stopServices = () => { Object.entries(services).forEach(([key, provider]) => provider.stop()); }; -export const { notify: notifyService } = services; +export const { notify: notifyService, platform: platformService } = services; 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 00000000000000..440e9523044c1c --- /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 = ( + coreSetup, + coreStart, + setupPlugins, + startPlugins +) => { + return { coreSetup, coreStart, setupPlugins, startPlugins }; +}; 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 40c017543147fc..bfa68b33908e01 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/reducers/workpad.js b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js index 12733680ed32d4..30f9c638a054fe 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/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts deleted file mode 100644 index afca15203b9702..00000000000000 --- 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 47c6478f66471d..df9923d9f11ecc 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/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d1e8892fa2c984..a1186e04ee27a6 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -54,7 +54,7 @@ export function maps(kibana) { emsLandingPageUrl: mapConfig.emsLandingPageUrl, kbnPkgVersion: serverConfig.get('pkg.version'), regionmapLayers: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', []), + tilemap: _.get(mapConfig, 'tilemap', {}), }; }, styleSheetPaths: `${__dirname}/public/index.scss`, diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 1b1fbf111fe04b..bb1a6b74d43a70 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -336,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]); } diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 3a4c7b71dcd03d..36030e1fa7f2a5 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -251,7 +251,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/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index a047c25c2b1d74..c6031cb2203341 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -5,6 +5,8 @@ */ import angular, { IWindowService } 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/x-pack/legacy/plugins/remote_clusters/common/index.ts b/x-pack/legacy/plugins/remote_clusters/common/index.ts deleted file mode 100644 index baad348d7a1365..00000000000000 --- 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 439cb818d8a562..00000000000000 --- 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 4ae11323642d8a..00000000000000 --- 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/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 5a04f1a497abf6..0d75b067fbe630 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 519a3289395b93..c6861ae1d17add 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; + export function screenshotsObservableFactory( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory -) { +): ScreenshotsObservableFn { return function screenshotsObservable({ logger, urls, 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 c9cba64a732b6f..9d3deda5d98bec 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) => { 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 113da92d1862f0..0ffd42d0b52f9c 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 = 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 a15541d99f6fb5..c03ea170f76eee 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 c3c0d38584bc13..ae18c0f4f9f4ba 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 dbdccb6160a6e6..3d69042b6c7abe 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 = 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 a62b7ec7013a59..c882ef682f9529 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 9be61d091b00e8..0b243f13adb80b 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 { - return (await this.getPluginSetupDeps()).browserDriverFactory; - } - public getConfig(): ReportingConfig { return this.config; } + public async getScreenshotsObservable(): Promise { + 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 c564963e363cc2..4288a37fe6adc4 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 8230ee889ae057..560cc943ed45e8 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( logger: Logger ): Promise { 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 5a062a693b4681..3e87337dc43550 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( 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 00000000000000..aa22c3f66df189 --- /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 359bcc45230c34..ef985d2dd1cf3c 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, @@ -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 e9523d9e702029..2c3bb8f4bf71ca 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(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); + const pdfLayoutBuckets = get(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(aggs[STATUS_TYPES_KEY], 'buckets', []); if (statusBuckets) { statusTypes = getKeyCount(statusBuckets); } - return { _all: all, status: statusTypes, ...jobTypes }; + let statusByApp = {}; + const statusAppBuckets = get(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 { - const buckets = get(response, 'aggregations.ranges.buckets'); + +async function handleResponse(response: SearchResponse): Promise { + const buckets = get(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 dbc674ce36ec85..61b736a3e4d8cf 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 98e025ccf661ee..83f17018633551 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 3647be38b6a09e..00000000000000 --- 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 526af055a3ef65..00000000000000 --- 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 f33ae7cfee0a22..00000000000000 --- 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 78458c9218be3c..00000000000000 --- 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 6bbd00ac6576e0..00000000000000 --- 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 eb16b211da3fd8..00000000000000 --- 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 883b3552a7c020..00000000000000 --- 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 787814d87dff94..00000000000000 --- 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/is_es_error/index.ts b/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts deleted file mode 100644 index a9a3c61472d8c7..00000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/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 { isEsError } from './is_es_error'; 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 0743e443955f45..00000000000000 --- 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 b6cea09e0ea3c1..00000000000000 --- 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 353510d96a00d4..00000000000000 --- 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 -): RequestHandler => { - 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 05c22b030fff9c..00000000000000 --- 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 { - 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 146c3e973f9ead..00000000000000 --- 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 2516840bd95375..00000000000000 --- 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 = 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 e78f09a71876b1..00000000000000 --- 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 = 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 = 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 e45713e2b807c1..00000000000000 --- 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 = 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 = 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 = 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 = 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 = 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 97999a4b5ce8d2..00000000000000 --- 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 = 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 941610b97707f8..00000000000000 --- 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 bcc6770e9b8ee0..00000000000000 --- 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 618c6c3e97b576..00000000000000 --- 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 5b2218af1fd521..b1dec2ce82c520 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) => // 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 a4071d39e63c0f..00000000000000 --- 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 d572561944a76d..00000000000000 --- a/x-pack/legacy/plugins/siem/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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { Root } from 'joi'; - -import { APP_ID, APP_NAME } 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, - }, - ], - }, - 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 c9b3f8a37a7ead..00000000000000 --- a/x-pack/legacy/plugins/siem/package.json +++ /dev/null @@ -1,16 +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" - }, - "dependencies": { - "lodash": "^4.17.15", - "react-markdown": "^4.0.6" - } -} 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 01175a98d1e44e..00000000000000 --- 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(, 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 778adc708d9012..00000000000000 --- 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$(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 ( - <> - - - - ); -}; -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 a24c66e31e6708..00000000000000 --- 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 e2078bb2473f57..00000000000000 --- 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; - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -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 ( - - {type === 'and' ? i18n.AND : i18n.OR} - - ); -}); - -AndOrBadge.displayName = 'AndOrBadge'; 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 55e114818ffea1..00000000000000 --- 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: - '

Filter results that contain agent.ephemeral_id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.hostname ', - description: - '

Filter results that contain agent.hostname

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.id ', - description: - '

Filter results that contain agent.id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.name ', - description: - '

Filter results that contain agent.name

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.type ', - description: - '

Filter results that contain agent.type

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.version ', - description: - '

Filter results that contain agent.version

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test1 ', - description: - '

Filter results that contain agent.test1

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test2 ', - description: - '

Filter results that contain agent.test2

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test3 ', - description: - '

Filter results that contain agent.test3

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test4 ', - description: - '

Filter results that contain agent.test4

', - start: 0, - end: 1, - }, -]; - -describe('Autocomplete', () => { - describe('rendering', () => { - test('it renders against snapshot', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it is rendering with placeholder', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = mount( - - ); - const input = wrapper.find('input[type="search"]'); - expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); - }); - - test('Rendering suggested items', () => { - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - 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( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - 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( - - ); - - 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( - - - - ); - - 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( - - ); - - 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( - - ); - 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 f051e18f8acab6..00000000000000 --- 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 ( - - - - {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( - - {suggestions.map((suggestion, suggestionIndex) => ( - - ))} - - ) : null} - - - ); - } - - 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) => { - this.changeValue(evt.currentTarget.value); - }; - - private handleKeyDown = (evt: React.KeyboardEvent) => { - 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) => { - 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 = ( - prevState: Readonly, - prevProps: Readonly -) => State | null; - -function composeStateUpdaters(...updaters: Array>) { - 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 & - 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/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx deleted file mode 100644 index a7272593c2b27f..00000000000000 --- 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', -}))` - .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( - 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$(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 : ( - - - - - {i18n.EMBEDDABLE_HEADER_HELP} - - - - - - - - - - {embeddable != null ? ( - - ) : !isLoading && isIndexError ? ( - - ) : ( - - )} - - - ); -}; - -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 216fe9105327ca..00000000000000 --- 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; 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 c6d9dbc2fcfc89..00000000000000 --- 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 = ({ - 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(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 ( - - - - ); -}; - -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; - -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 b0f6494e2d663c..00000000000000 --- 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; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -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( - ({ 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 ( - <> - - - - - - - - ); - } -); - -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; - -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 abde602c1bdacc..00000000000000 --- 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$(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 56fa0d56f3c3a1..00000000000000 --- 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(); - - 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 acbb337a7c2ab8..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/header_page/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 { 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', -})` - ${({ 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any -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 = ({ - backOptions, - badgeOptions, - border, - children, - draggableArguments, - isLoading, - subtitle, - subtitle2, - title, - titleNode, - ...rest -}) => ( -
- - - {backOptions && ( - - - {backOptions.text} - - - )} - - {titleNode || ( - - )} - - {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/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx deleted file mode 100644 index 45225e31e9ac82..00000000000000 --- 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/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 5aa846d15b684b..00000000000000 --- 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 98437845a3ab7d..00000000000000 --- 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 991c82cf701e89..00000000000000 --- 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 5407eba8b5b29d..00000000000000 --- 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 899d108fe246d1..00000000000000 --- 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 a821d310344d8d..00000000000000 --- 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 b9563b60f301bb..00000000000000 --- 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 fe701ad115d17a..00000000000000 --- 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 96a70e0bc70cc1..00000000000000 --- 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 686ec4e86e7852..00000000000000 --- 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 6a5e08b287f96c..00000000000000 --- 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 946c4b3a612dd1..00000000000000 --- 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 04f0abe0d00d17..00000000000000 --- 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 c27a6039da29d9..00000000000000 --- 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 ca8fa50c572fe3..00000000000000 --- 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 b7cc92ebd183f1..00000000000000 --- 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 d88bc2bf3b7e6d..00000000000000 --- 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 127eb3bae0284a..00000000000000 --- 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 a0ca5f855237c5..00000000000000 --- 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 44dc75cd6bad37..00000000000000 --- 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; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -CountBadge.displayName = 'CountBadge'; - -export const Spacer = styled.span` - margin-left: 5px; -`; - -Spacer.displayName = 'Spacer'; - -export const Badge = styled(EuiBadge)` - vertical-align: top; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -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 a652fef5508fca..00000000000000 --- 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 b43efbbde51b33..00000000000000 --- 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 af50fa88e5fe8e..00000000000000 --- 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/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx deleted file mode 100644 index 2f743c33872090..00000000000000 --- 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 e481fe72452019..00000000000000 --- 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 49afc8d5ef68b3..00000000000000 --- 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 557d389aefee92..00000000000000 --- 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 5b851701b973c4..00000000000000 --- 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 a2e75e965bc76d..00000000000000 --- 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 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/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 cf350b3993a4b9..00000000000000 --- 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 c43c69da64ba40..00000000000000 --- 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 9fd71f071ec600..00000000000000 --- 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 f051bbe5b1af6a..00000000000000 --- 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/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx deleted file mode 100644 index bf953cfd006aa5..00000000000000 --- 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; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -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 a49f6cc930abda..00000000000000 --- 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 f53f7bb56e2f40..00000000000000 --- 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 a0a0ac4c2b85c0..00000000000000 --- 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 639d30bbe7bb90..00000000000000 --- 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 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] - ); - - 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 61772f1dc7a7ab..00000000000000 --- 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 983b234a04eaad..00000000000000 --- 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 b30244e57d0f1a..00000000000000 --- 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 83c38f2a76175f..00000000000000 --- 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 d17eadc68d04b6..00000000000000 --- 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 f698e302d34232..00000000000000 --- 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 13bb40dad04bd8..00000000000000 --- 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 6d2cfb7147537f..00000000000000 --- 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 b7453616324199..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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 { - CASE_STATUS_URL, - CASES_URL, - CASE_TAGS_URL, - CASE_REPORTERS_URL, - ACTION_TYPES_URL, - ACTION_URL, -} from '../../../../../../plugins/case/common/constants'; - -import { - getCaseDetailsUrl, - getCaseUserActionUrl, - getCaseCommentsUrl, -} from '../../../../../../plugins/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: 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/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 03f7d241e5dffd..00000000000000 --- 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 85e472811c93bf..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ /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 { isEmpty } from 'lodash/fp'; -import { - Connector, - CasesConfigurePatch, - CasesConfigureResponse, - CasesConfigureRequest, -} from '../../../../../../../plugins/case/common/api'; -import { KibanaServices } from '../../../lib/kibana'; - -import { - CASE_CONFIGURE_CONNECTORS_URL, - CASE_CONFIGURE_URL, -} from '../../../../../../../plugins/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/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 d2491b39fdf561..00000000000000 --- 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 d69c23fe02ec9e..00000000000000 --- 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 3ee16e19eaf9f0..00000000000000 --- 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 1c03a09a8c2eaf..00000000000000 --- 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/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts deleted file mode 100644 index 0bda75e5bc9e05..00000000000000 --- 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 e552f22b55fa40..00000000000000 --- 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 1ec98bf5b5f1f4..00000000000000 --- 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 69f4c93a82e2cc..00000000000000 --- 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 2f2de2e151664e..00000000000000 --- 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/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts deleted file mode 100644 index ece2483adde3ad..00000000000000 --- 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 8628ba502f0819..00000000000000 --- 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 5ef8e67dedddbf..00000000000000 --- 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 67cfe259927ab2..00000000000000 --- 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 7ff9577bfb05e7..00000000000000 --- 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 5806125f2397b9..00000000000000 --- 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 7376f38ae8d0fd..00000000000000 --- 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 edf3f6855f955f..00000000000000 --- 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 405c45348b54da..00000000000000 --- 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 954bfede07139e..00000000000000 --- 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 3933aefa60483c..00000000000000 --- 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 7035d631931188..00000000000000 --- 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 002a819417df64..00000000000000 --- 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 af4eb1ff7a5e1f..00000000000000 --- 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 55d7e7cdc6e543..00000000000000 --- 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 060b66fc3cbbef..00000000000000 --- 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 b13637fa88d07b..00000000000000 --- 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 17a14ce3a11205..00000000000000 --- 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 fdac282292a4bc..00000000000000 --- 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 e7b68bf557a219..00000000000000 --- 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 c7f72ac6193f40..00000000000000 --- 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 3467e2b5f18d84..00000000000000 --- 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 805c69f7fcc12a..00000000000000 --- 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 32ac62d594e1c9..00000000000000 --- 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 b5c91ca287f0b8..00000000000000 --- 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 0debed9c5f9aae..00000000000000 --- 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 3c089ef6926dde..00000000000000 --- 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 20617b88bda941..00000000000000 --- 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 72e4e46bc6ae03..00000000000000 --- 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 658cb5785b54c6..00000000000000 --- 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 e15c099a007ad3..00000000000000 --- 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 301b8bd6553337..00000000000000 --- 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 3a396a0637ea12..00000000000000 --- 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 b3a06a170bb807..00000000000000 --- 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/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts deleted file mode 100644 index 66326a6590debc..00000000000000 --- 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 53f845de48fb31..00000000000000 --- 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 227680d79912f4..00000000000000 --- 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 012a1cfef5da2d..00000000000000 --- 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 3a6a3f13dc5cec..00000000000000 --- 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/mock/kibana_core.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts deleted file mode 100644 index cf3523a6260bbf..00000000000000 --- 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 db7a931b3fb153..00000000000000 --- 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 74f6411f17fa06..00000000000000 --- 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 eaba708948a993..00000000000000 --- 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 c61874a8dabfc1..00000000000000 --- 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/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 135f0f2a7e26d7..00000000000000 --- 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 5ea3f500c0349f..00000000000000 --- 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 241dcef14a145d..00000000000000 --- 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 df958b75dc6b89..00000000000000 --- 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 f6b9d38a76de30..00000000000000 --- 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 0897be6310fa2e..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.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 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/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 0f819f961b3963..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or 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 '../../../../../../../../plugins/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/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx deleted file mode 100644 index 4653dbc67d5a18..00000000000000 --- 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/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 b5627376ac8aa1..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or 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/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 77215e2318ded1..00000000000000 --- 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 5092cba6872e36..00000000000000 --- 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 d6016e540bdc08..00000000000000 --- 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/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts deleted file mode 100644 index df9f0d08e728c5..00000000000000 --- 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 7e6778ca4fb4f1..00000000000000 --- 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 d80ef066d86ac2..00000000000000 --- 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 ce8ae2054b2c77..00000000000000 --- 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 847fcc78600851..00000000000000 --- 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 90bdd39e4a6faf..00000000000000 --- 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 e70ba804ec0187..00000000000000 --- 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 5e0293325289be..00000000000000 --- 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/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 af946c6f02cbbb..00000000000000 --- 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 d3014354ab7b35..00000000000000 --- 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; - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -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 8e8927cb7bbd13..00000000000000 --- 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 49977713a585ad..00000000000000 --- 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 bfca6b20684432..00000000000000 --- 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 82350150488d05..00000000000000 --- 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 d232c86c19e6fd..00000000000000 --- 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 b4d813c48b43f2..00000000000000 --- 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 2b1e5a367a9651..00000000000000 --- 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 be9e919b806b5f..00000000000000 --- 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 629c6758a14147..00000000000000 --- 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 3b297a623e34d5..00000000000000 --- 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 a65e8178f75c42..00000000000000 --- 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 1c01a19573cd67..00000000000000 --- 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 7bea41c2ab4d54..00000000000000 --- 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 380ef521903490..00000000000000 --- 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 11942c57a4d279..00000000000000 --- 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 a8a34383585c6a..00000000000000 --- 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 69268d841e12ee..00000000000000 --- 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 461fde2bba0caa..00000000000000 --- 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 a12c95b3b5a6fe..00000000000000 --- 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 c4680dc3d795ef..00000000000000 --- 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 ebcb07131bb247..00000000000000 --- 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 207b86fee02b93..00000000000000 --- 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 e796eaca0cd28e..00000000000000 --- 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 efd9c644ec6b6f..00000000000000 --- 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 1488dd556c0b9d..00000000000000 --- 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 a5d0207f526d10..00000000000000 --- 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 90c18b6ff69f46..00000000000000 --- 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 8e09572cb27966..00000000000000 --- 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 0fc37935b6062a..00000000000000 --- 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 14cc29adb505a4..00000000000000 --- 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 feba80539a11b9..00000000000000 --- 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/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx deleted file mode 100644 index da4aad97e5b485..00000000000000 --- 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 b5e8f78ebc560f..00000000000000 --- 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 0c0ac637a42293..00000000000000 --- a/x-pack/legacy/plugins/siem/public/shared_imports.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. - */ - -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/inputs/actions.ts b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts deleted file mode 100644 index 5b26957843f082..00000000000000 --- 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 e851caf523eb4b..00000000000000 --- 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 9e9e663a59fe09..00000000000000 --- 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 51043b999c27e9..00000000000000 --- 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 fa70c1b04608da..00000000000000 --- 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 ef46a8d061c2e8..00000000000000 --- 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 a8ee10ba2b8014..00000000000000 --- 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 4b16253de2abe6..00000000000000 --- 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 3ea687f7003f40..a3167920efa06c 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'; @@ -21,19 +19,6 @@ import { ArrayOrItem, } from '../../../../../src/legacy/plugin_discovery/types'; -const savedObjectSchemas = { - task: { - hidden: true, - isNamespaceAgnostic: true, - convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, - // legacy config is marked as any in core, no choice here - // eslint-disable-next-line @typescript-eslint/no-explicit-any - indexPattern(config: any) { - return config.get('xpack.task_manager.index'); - }, - }, -}; - export function taskManager(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSpec> { return new kibana.Plugin({ id: 'task_manager', @@ -58,7 +43,7 @@ export function taskManager(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSp 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 @@ -77,10 +62,5 @@ export function taskManager(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSp ) ); }, - uiExports: { - mappings, - migrations, - savedObjectSchemas, - }, } as Legacy.PluginSpecOptions); } 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 eb74290c84682b..00000000000000 --- 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 8c83c0a571f28d..00000000000000 --- 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 b5e8ce47502155..00000000000000 --- 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 885ac4d5a9b44f..00000000000000 --- 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/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts deleted file mode 100644 index 00781726941d5c..00000000000000 --- 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 22df610d2d516c..00000000000000 --- 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 e69de29bb2d1d6..00000000000000 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 78aab3806ae049..00000000000000 --- 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 a32eabd49a3e5e..00000000000000 --- 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 f52ad8ce867b61..00000000000000 --- 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 d58bf8398fcdea..00000000000000 --- 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 e73598c44c9f08..00000000000000 --- 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 85d0b1b593704a..00000000000000 --- 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/contexts/index.ts b/x-pack/legacy/plugins/uptime/public/contexts/index.ts deleted file mode 100644 index 2b27fcfe907abd..00000000000000 --- 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/lib/alert_types/index.ts b/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts deleted file mode 100644 index 74160577cb0b13..00000000000000 --- 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 d8c2a780928541..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.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 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/legacy/plugins/uptime/public/register_feature.ts b/x-pack/legacy/plugins/uptime/public/register_feature.ts deleted file mode 100644 index 2f83fa33ba4bc5..00000000000000 --- 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/state/actions/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts deleted file mode 100644 index 67c75314a7305b..00000000000000 --- 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 { 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/types.ts b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts deleted file mode 100644 index 41381afd31453f..00000000000000 --- 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 a5adb96731f330..00000000000000 --- 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 16b90e99214284..00000000000000 --- 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 { 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'; - -export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); - -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; - - // 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 e67efa8570c110..00000000000000 --- 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/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts deleted file mode 100644 index df5e825c3488b9..00000000000000 --- 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 { 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(), -}; - -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 88995a2f5dd70c..00000000000000 --- 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/tsconfig.json b/x-pack/legacy/plugins/uptime/tsconfig.json deleted file mode 100644 index 53425909db3e8b..00000000000000 --- 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/package.json b/x-pack/package.json index d0025ff436649d..dcc9b8c61cb960 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": { @@ -100,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", @@ -121,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", @@ -339,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 d6c85606edc2c5..decd170ca5dd64 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -143,7 +143,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) | 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 6a806d1fa531ca..d14d0ca2ddf84a 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 26bd68adfc4b66..3be2f265570791 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 735ec349837a94..723982b11e1ccd 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 */ diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 14441bfd52dd73..c96c993fef606f 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); 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 265ff267f222ed..18b434e980eb93 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: unknown) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; 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 ed57e44c3f0b3c..be60f4c2f28afd 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; @@ -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 32f5e230157002..899684367d52d3 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,7 +72,7 @@ async function executor( bulkBody.push(document); } - const bulkParams: unknown = { + const bulkParams = { index, body: bulkBody, refresh: config.refresh, 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 ac21905ede11c7..9150633f06117f 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/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index e54c8179ae7b43..1bca7c18e4e1b5 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: unknown) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; 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 3ce01c59596f6d..d5a9c0cc1ccd2a 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: unknown) => {}, - 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/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 08b837cf8d0a5d..a6c3ae88765ac0 100644 --- 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 @@ -7,9 +7,9 @@ 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 { actionsMock } from '../../mocks'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; @@ -21,10 +21,7 @@ jest.mock('./action_handlers'); const handleIncidentMock = handleIncident as jest.Mock; -const services: Services = { - callCluster: async (path: string, opts: unknown) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; 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 22c4b63474fdc8..cbcd4b29545183 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 @@ -10,17 +10,14 @@ import { ActionTypeExecutorOptions, ActionTypeExecutorResult, } from '../types'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; 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: unknown) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; 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 1beaf92f3f48b4..d28856954cca5a 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: unknown) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; 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 6bdd30848e4b73..1b7752588e3d39 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 ac324587c5f4a0..db38431b02cacd 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -70,7 +70,9 @@ export function createExecuteFunction({ 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 124e5951c714bb..4594fc1ddf6d9a 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 ac574decdba715..101e18f2583e39 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -90,7 +90,9 @@ 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, unknown>; @@ -138,13 +140,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'; diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index bc4268bb698723..4160ace50f4916 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.ts b/x-pack/plugins/actions/server/plugin.ts index 280c14ca8c058e..a6cc1fb5463bbb 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[]; @@ -131,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({ @@ -170,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; @@ -271,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) { @@ -297,6 +292,9 @@ 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; + }, }); } 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 00000000000000..dbd7925f968713 --- /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 9dcfdb81f5ebb5..093d22c2c1a710 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SavedObjectsClientContract, - SavedObjectAttributes, - KibanaRequest, -} 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: KibanaRequest) => Services; @@ -21,8 +23,9 @@ export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export interface Services { - callCluster(path: string, opts: unknown): Promise<unknown>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } declare module 'src/core/server' { diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 177e42de5a95b6..62c2caed669af7 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/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index a9e224142a6325..c94a7aba46cfa3 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.ts b/x-pack/plugins/alerting/server/plugin.ts index 35ebafce9dc677..8cdde2eeb9877a 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 = { @@ -133,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({ @@ -270,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/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts new file mode 100644 index 00000000000000..4efec2fe55ef01 --- /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/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 756080baba6266..0e46ef4919626a 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 () => { @@ -111,6 +112,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 +150,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 72f9e70905dc28..5c3e36b88879dd 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 @@ -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` ); 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 31cc893f785cb1..26d8a1d1777c04 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,6 +165,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -185,6 +183,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,6 +227,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -344,6 +344,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -560,6 +561,7 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "outcome": "failure", }, "kibana": Object { "saved_objects": Array [ 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 9c8cf4b1c968df..26970dc6b2b0d9 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -202,12 +202,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 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 1d220f97f127a8..563664d3544ac4 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/types.ts b/x-pack/plugins/alerting/server/types.ts index bc98cae65b4e6a..b733b23dd71e6e 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,14 +7,16 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; +import { Alert, AlertActionParams, ActionGroup } from '../common'; +import { AlertsClient } from './alerts_client'; +export * from '../common'; import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, SavedObjectAttributes, SavedObjectsClientContract, - KibanaRequest, } from '../../../../src/core/server'; -import { Alert, AlertActionParams, ActionGroup } from '../common'; -import { AlertsClient } from './alerts_client'; -export * from '../common'; // This will have to remain `any` until we can extend Alert Executors with generics // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -36,10 +38,9 @@ declare module 'src/core/server' { } export interface Services { - // This will have to remain `any` until we can extend Alert Services with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callCluster(path: string, opts: any): Promise<any>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } export interface AlertServices extends Services { 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 index f4bda2af653aac..0e4586f707f426 100644 --- 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 @@ -4,6 +4,7 @@ * 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'; @@ -12,7 +13,7 @@ import { setupEnvironment, pageHelpers, nextTick } from './helpers'; const { setup } = pageHelpers.followerIndexEdit; const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; -describe('Edit Auto-follow pattern', () => { +describe('Edit follower index', () => { let server; let httpRequestsMockHelpers; @@ -88,6 +89,53 @@ describe('Edit Auto-follow pattern', () => { }); }); + 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; 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 a7d6aa894d91db..6c635cc5b4489a 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 @@ -96,6 +96,34 @@ describe('Async search strategy', () => { }); }); + // 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, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + 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, is_running: true, is_partial: true })) 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 00000000000000..85e1643d684f2c --- /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 1978d780f54f5e..e40fc3e386bc89 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 650486f3c3858d..47f39d2d117976 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 7b48780f2d86bb..cffc00661515f6 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 00000000000000..f21307e407fd0a --- /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 49f8ebbd580d85..8fce15d1c794c8 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 90d6b8b82198af..6bc728db99819c 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 56a49df3bdab48..16a1f96c926b80 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 @@ -5,7 +5,7 @@ */ import { ServerApiError } from '../../types'; -import { HostResultList, HostInfo } from '../../../../../common/types'; +import { HostResultList, HostInfo, GetHostPolicyResponse } from '../../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; @@ -27,8 +27,14 @@ interface ServerFailedToReturnHostDetails { payload: ServerApiError; } +interface ServerReturnedHostPolicyResponse { + type: 'serverReturnedHostPolicyResponse'; + payload: GetHostPolicyResponse; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList | ServerReturnedHostDetails - | ServerFailedToReturnHostDetails; + | ServerFailedToReturnHostDetails + | ServerReturnedHostPolicyResponse; 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 515c54eab32806..863ffc50d0155e 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 @@ -57,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 1af83a975d1d87..2064c76f7dfb51 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 @@ -62,6 +62,6 @@ describe('host list middleware', () => { 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 bb1cfc4dd10af1..d1b9a2cde4b319 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 @@ -8,6 +8,7 @@ import { HostResultList } from '../../../../../common/types'; import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; import { HostState } from '../../types'; import { ImmutableMiddlewareFactory } from '../../types'; +import { HostPolicyResponse } from '../../../../../common/types'; export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = coreStart => { return ({ getState, dispatch }) => next => async action => { @@ -69,6 +70,21 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = core type: 'serverReturnedHostDetails', payload: response, }); + // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call + dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + 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 adf18fa50c24ff..93e995194353b3 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 @@ -20,6 +20,7 @@ const initialState = (): HostState => { details: undefined, detailsLoading: false, detailsError: undefined, + policyResponse: undefined, location: undefined, }; }; @@ -37,7 +38,7 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = ( } = action.payload; return { ...state, - hosts: hosts.map(hostInfo => hostInfo.metadata), + hosts, total, pageSize, pageIndex, @@ -63,6 +64,11 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = ( detailsError: action.payload, detailsLoading: false, }; + } else if (action.type === 'serverReturnedHostPolicyResponse') { + return { + ...state, + policyResponse: action.payload.policy_response, + }; } else if (action.type === 'userChangedUrl') { const newState: Immutable<HostState> = { ...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 b0f949ebbe757a..b0711baf9cdfff 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 @@ -86,3 +86,13 @@ export const showView: (state: HostState) => 'policy_response' | 'details' = cre 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 69b11fb3c1f0ea..9912c9a81e6e14 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 6d2e952fa07bba..4986a342cca19e 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 c2865d36c95f2d..46f4c09e05a745 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 4356517e43c2cb..5c27680d6a35c7 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 e5e600f6c62884..58e706f20ec8e0 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'; @@ -90,7 +91,7 @@ export type SubstateMiddlewareFactory = <Substate>( export interface HostState { /** list of host **/ - hosts: HostMetadata[]; + hosts: HostInfo[]; /** number of items per page */ pageSize: number; /** which page to show */ @@ -107,6 +108,8 @@ export interface HostState { 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>; } @@ -308,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 d18bc59a35f52e..d32ad4dd9defc6 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/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index e4da39d50304a8..7d948f54bd0bce 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,12 +16,12 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -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 { 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)` @@ -31,9 +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 = useHostSelector(uiQueryParams); + const policyStatus = useHostSelector( + policyResponseStatus + ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; const detailsResultsUpper = useMemo(() => { return [ { @@ -79,7 +90,10 @@ 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" @@ -87,8 +101,9 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { 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> @@ -126,6 +141,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { details.host.ip, policyResponseUri.search, policyStatusClickHandler, + policyStatus, ]); return ( 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 11dbed716c5277..5a8765110c9097 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,7 +181,6 @@ 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( '?page_index=0&page_size=10&selected_host=1&show=policy_response' ); @@ -154,6 +197,58 @@ describe('when on the hosts page', () => { '?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(); const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); @@ -176,7 +271,7 @@ describe('when on the hosts page', () => { 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(); 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 5c2922162ce0ca..026ba2ff151264 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 @@ -16,10 +16,20 @@ import * as selectors from '../../store/hosts/selectors'; 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; @@ -73,20 +83,40 @@ export const HostList = () => { [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 ( + <EuiHealth + color={HOST_STATUS_TO_HEALTH_COLOR[hostStatus]} + data-test-subj="rowHostStatus" + > + <FormattedMessage + id="xpack.endpoint.host.list.hostStatusValue" + defaultMessage="{hostStatus, select, online {Online} error {Error} other {Offline}}" + values={{ hostStatus }} + /> + </EuiHealth> + ); + }, + }, { field: '', name: i18n.translate('xpack.endpoint.host.list.policy', { @@ -117,26 +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: '', @@ -158,7 +185,7 @@ export const HostList = () => { headerLeft={i18n.translate('xpack.endpoint.host.hosts', { defaultMessage: 'Hosts' })} > {hasSelectedHost && <HostDetailsFlyout />} - <EuiText color="subdued" size="xs"> + <EuiText color="subdued" size="xs" data-test-subj="hostListTableTotal"> <FormattedMessage id="xpack.endpoint.host.list.totalCount" defaultMessage="{totalItemCount, plural, one {# Host} other {# Hosts}}" 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 ea9eb292dba1a9..d9bb7eabcf7b02 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 @@ -52,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) { @@ -79,7 +79,7 @@ export const PolicyDetails = React.memo(() => { }); } } - }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]); + }, [notifications.toasts, policyName, policyUpdateStatus]); const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); 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 7f946de9614ca7..9d73f12869058f 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 14871c71ec0386..4b2a6ece9f58fc 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/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 373afa89921dcd..3ec15f2f1985da 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 f01136fe20ebf9..f95ecc63d2a666 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 a3184389a794e8..fc307002819a92 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 5dda54d4ed0298..59ee4b3b875055 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 4e57212e5c0c29..c7177c6387e7a9 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 e8ae3d08e5cb61..7d09d90881da94 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 d370bda0d18424..17aa598720c593 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/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 36155ece57a9c4..2e7ca65c92dc1e 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/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 6e83fc19a922e2..1545e9c2ae6e87 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/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index dcedd27fc5a3d4..903d48746bfb31 100644 --- a/x-pack/plugins/endpoint/server/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; -import { EndpointAppConstants } from '../common/types'; +import { AlertConstants } from '../common/alert_constants'; import { ESIndexPatternService } from '../../ingest_manager/server'; export interface IndexPatternRetriever { @@ -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/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index e438ab853f3b52..92f8aacbf26a29 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,7 +23,7 @@ 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>; 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 d482da03872c62..0f69e1bb60c44e 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 09de11897813b5..b61f90b5b17f5a 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 74db24c85eab5e..7bc1c0c306ae2b 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/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 95c8e4662cfcea..92bd07a813d264 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) @@ -67,7 +67,7 @@ export const getRequestData = async ( reqData.searchBefore[0] === '' && reqData.emptyStringIsUndefined ) { - reqData.searchBefore[0] = EndpointAppConstants.MAX_LONG_INT; + reqData.searchBefore[0] = AlertConstants.MAX_LONG_INT; } if ( @@ -75,7 +75,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 cf4eac5d25f800..5aefc35b5758f1 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 79083f5f05e149..7e78caaf178e46 100644 --- a/x-pack/plugins/endpoint/server/routes/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.ts @@ -6,7 +6,8 @@ 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'; function handleIndexPattern( @@ -33,7 +34,7 @@ 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 }, }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index a96d431225b150..3599acacb4f59c 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -6,20 +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'); router.get( { - path: '/api/endpoint/resolver/{id}/related', - validate: validateRelatedEvents, + path: '/api/endpoint/resolver/{id}/events', + validate: validateEvents, options: { authRequired: true }, }, - handleRelatedEvents(log, endpointAppContext) + handleEvents(log, endpointAppContext) ); router.get( @@ -31,12 +38,21 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp 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, endpointAppContext) + 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 00000000000000..6648dc5b9e4938 --- /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 c3b19b6c912b61..bb18b29a4b9479 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -4,87 +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 { TypeOf } from '@kbn/config-schema'; +import { validateChildren } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; import { EndpointAppContext } from '../../types'; -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()), - }), -}; - export function handleChildren( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler<ChildrenPathParams, ChildrenQueryParams> { +): 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 indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - const pagination = getPaginationParams(limit, after); 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 00000000000000..a70a6e8d097d08 --- /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 91a4c5d49bc54d..00000000000000 --- 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 { 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 { EndpointAppContext } from '../../types'; - -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, - endpointAppContext: EndpointAppContext -): RequestHandler<LifecyclePathParams, LifecycleQueryParams> { - return async (context, req, res) => { - const { - params: { id }, - query: { ancestors, legacyEndpointID }, - } = req; - try { - const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - 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 b049439207e508..eba4e5581c1367 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 e73053d53dee08..2a097e87c38b29 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 6d084a0cf20e5e..690c926d7e6d6d 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 00000000000000..78e5ee9226581d --- /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 00000000000000..b622cb8a211111 --- /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 00000000000000..01e818c89b3efc --- /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 8a3955706b2781..296135af83b72e 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 290c601e0e9d8c..e775b0cf9b6d2c 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 5caef935ce6214..00000000000000 --- 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 cc5afe8face8d5..00000000000000 --- 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 00000000000000..17a158aec7cf56 --- /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 00000000000000..7db3ab2b0cb1fe --- /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 83e111a1e62e66..00000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.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 { schema } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { getPaginationParams } from './utils/pagination'; -import { RelatedEventsQuery } from './queries/related_events'; -import { EndpointAppContext } from '../../types'; - -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, - endpointAppContext: EndpointAppContext -): RequestHandler<RelatedEventsPathParams, RelatedEventsQueryParams> { - return async (context, req, res) => { - const { - params: { id }, - query: { limit, after, legacyEndpointID }, - } = req; - try { - const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - 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 00000000000000..25f15586341d5e --- /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 00000000000000..7315b4ee6c618f --- /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 6d5ac8efdc1da5..00000000000000 --- 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 5a64f3ff9ddb61..20249b81660bba 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 00000000000000..5a55c23b908733 --- /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/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 9c1dff60f9727a..f487e9262e50e7 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" } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 5e93f320c009f2..9c923fe77d0358 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( diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index de3c9d631fbca6..8cc2c74b60e57b 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -53,6 +53,7 @@ exports.EcsEventLogProperties = [ 'event.start', 'event.duration', 'event.end', + 'event.outcome', // optional, but one of failure, success, unknown 'error.message', 'user.name', 'kibana.server_uuid', 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 470123ada48ea4..f79962a3241317 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 @@ -226,7 +226,7 @@ describe('queryEventsBySavedObject', () => { body: { from: 0, size: 10, - sort: { 'event.start': { order: 'asc' } }, + sort: { '@timestamp': { order: 'asc' } }, query: { bool: { must: [ @@ -340,7 +340,7 @@ describe('queryEventsBySavedObject', () => { }, { range: { - 'event.start': { + '@timestamp': { gte: start, }, }, @@ -409,14 +409,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 6d5c6b31a637c3..47d273b9981e32 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 @@ -176,14 +176,14 @@ export class ClusterClientAdapter { }, start && { range: { - 'event.start': { + '@timestamp': { gte: start, }, }, }, end && { range: { - 'event.end': { + '@timestamp': { lte: end, }, }, 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 6d4c9b67abc1ba..17c073c4b27f9b 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 765f0895f8e0d1..8ef245e60aadc4 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/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 35ec0bb2bf6ce1..fa479b1b06d5ef 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -10,7 +10,7 @@ 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'; 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 00000000000000..cbfffbfd8f940c --- /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/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index d532c079e3e9cc..1c5a2f0fe1ad98 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 00000000000000..bb79b5ebc52905 --- /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 00000000000000..ae872cee9aa56f --- /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 00000000000000..5fd1b5efec1775 --- /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 00000000000000..e8bf63843c623d --- /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 00000000000000..fd312d7429b606 --- /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/runtime_types.ts b/x-pack/plugins/infra/common/runtime_types.ts index d5b858df38def1..a8d5cd8693a3da 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/public/components/alerting/logs/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx new file mode 100644 index 00000000000000..dd888639b6d073 --- /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 00000000000000..b18c2e5b8d69c7 --- /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 00000000000000..a9b45a117c2817 --- /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 00000000000000..e8cafecd94db11 --- /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 00000000000000..308165ce08a9b1 --- /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 00000000000000..3aed0db53bf2ca --- /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 00000000000000..8b0fd5eb721b37 --- /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 00000000000000..18126ec583696a --- /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 00000000000000..c8c513f57a9d73 --- /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/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index cdefffeb35c156..d4d53b81109c61 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useEffect, useState } from 'react'; +import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -13,6 +13,7 @@ import { EuiText, EuiFormRow, EuiButtonEmpty, + EuiFieldSearch, } from '@elastic/eui'; import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -147,7 +148,7 @@ export const Expressions: React.FC<Props> = props => { const onGroupByChange = useCallback( (group: string | null) => { - setAlertParams('groupBy', group || undefined); + setAlertParams('groupBy', group || ''); }, [setAlertParams] ); @@ -215,10 +216,20 @@ export const Expressions: React.FC<Props> = props => { } setAlertParams('sourceId', source?.id); } else { - setAlertParams('criteria', [defaultExpression]); + 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>) => onFilterQuerySubmit(e.target.value), + [onFilterQuerySubmit] + ); + return ( <> <EuiSpacer size={'m'} /> @@ -273,48 +284,52 @@ export const Expressions: React.FC<Props> = props => { <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".', - })} + <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={onFilterQuerySubmit} + value={alertParams.filterQuery} + /> + )) || ( + <EuiFieldSearch + onChange={handleFieldSearchChange} + value={alertParams.filterQuery} fullWidth - compressed - > - <MetricsExplorerGroupBy - onChange={onGroupByChange} - fields={derivedIndexPattern.fields} - options={{ - ...options, - groupBy: alertParams.groupBy || undefined, - }} - /> - </EuiFormRow> - </> - )} + /> + )} + </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/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 e02346c4e758a8..976e4165eb6d55 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 7d7df796d13adb..5c20df000ae512 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 2c389b47fa6cf2..f89aaf12db1bc6 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 69a6abbca4b347..0eb6140c0de84f 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/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 98825567cc204b..66f64aee24f6ee 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/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 267abe631c1423..b0981f9b3c41f2 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 00000000000000..786cb485b38dd5 --- /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 00000000000000..2f1d15ffaf4d3d --- /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 00000000000000..848801ab3c7ce7 --- /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 00000000000000..5d9c6373050a19 --- /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 00000000000000..8332018fddf901 --- /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 14da2b47bcfa2d..625a1ead4d930f 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 00000000000000..0110c55c7c556b --- /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 00000000000000..bc719cbd694e4f --- /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/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 8c522bb7fa764a..dec8eaae56f41f 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -26,12 +26,20 @@ 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 { prompt } = useNavigationWarningPrompt(); const prefixer = usePrefixPathWithBasepath(); const navigateToApp = useKibana().services.application?.navigateToApp; + const { hrefOnly } = options; const encodedSearch = useMemo(() => { return search ? encodeSearch(search) : undefined; @@ -86,7 +94,10 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L return { href, - onClick, + // 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, }; }; 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 ed1aa9e72ebaea..04b472ceb59c84 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 619cea6eda8b7e..cecea733b49e4c 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 2f34e62d8e611a..fc07289f02fe7f 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 67c8ea7660a260..e91ef87bdf34ae 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 ed6f06deeef641..2974939a83215c 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 24c1598787a204..d2db5002f4aa22 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 faee7a643085a3..00000000000000 --- 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 00000000000000..6e68debceac701 --- /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 00000000000000..ac3b75ad97bb2a --- /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 00000000000000..ebdda7ebbd5873 --- /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 00000000000000..a97e38884a5bd0 --- /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 00000000000000..83effaa3d51a55 --- /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 00000000000000..0beccffe5f4e8d --- /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 00000000000000..777f611ef33f71 --- /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 00000000000000..92d70955c678f1 --- /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 00000000000000..88b1441f0ba7c1 --- /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 3e07dcebf112d6..712d625052140a 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 010a17dae4ebdf..40ac5c74a6836f 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 b6061203a1c727..85781c48f95129 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 e4ccdaf7c57487..428a7d3fdfe4b1 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 2f9a76fd47490b..9667272eb24179 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 00000000000000..9e0f7d5035aaf4 --- /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/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3b6647b9bfbbeb..40366b2a54f243 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -21,7 +21,8 @@ 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 getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; +import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; export type ClientSetup = void; export type ClientStart = void; @@ -52,7 +53,8 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); - pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getMetricsAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); core.application.register({ id: 'logs', diff --git a/x-pack/plugins/infra/public/utils/datemath.test.ts b/x-pack/plugins/infra/public/utils/datemath.test.ts index 0f272733c5f972..c8fbe5583db2e2 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 50a9b6e4f69458..7331a2450956fd 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/use_viewport_dimensions.ts b/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts new file mode 100644 index 00000000000000..ddaf8fcd31a392 --- /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 1fe1431392a389..f880eca933241c 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -93,7 +93,10 @@ 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; }, diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 88b78dfd3e41c3..4ed30380dc164d 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/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index eda1fbfa5f4ce1..eed7d39b8e74ae 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/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 00000000000000..cdec04ab81a8e5 --- /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 00000000000000..04207a4233dfd6 --- /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/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index a52659dae01f12..24b6ba2ec378bc 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 @@ -20,7 +20,7 @@ const executor = createMetricThresholdExecutor('test') as (opts: { }) => Promise<void>; const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation((_: string, { body, index }: any) => { +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) { 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 b697af4fa4c3b2..3415ae9873bfb1 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 @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerting/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; +import { InfraBackendLibs } from '../../infra_types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) => @@ -17,7 +18,10 @@ const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) => arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, }); -export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { +export async function registerMetricThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { if (!alertingPlugin) { throw new Error( 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' 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 6ec6f31256b787..9760873ff74781 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,15 @@ import { PluginSetupContract } from '../../../../alerting/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_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]; + const registerFns = [registerMetricThresholdAlertType, registerLogThresholdAlertType]; registerFns.forEach(fn => { - fn(alertingPlugin); + fn(alertingPlugin, libs); }); } }; 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 d2e151ca2c3f52..ecbc71f4895c79 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 528b9a69327fa4..07bc965dda77a6 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/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 1f0845b6b223f6..9bb953845e5a17 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 4a83ca730ff83f..57efb0f676b2fe 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 99e062aa49ccf3..0368c7bfd6db81 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 { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -37,7 +37,7 @@ export class InfraSources { } public async getSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ): Promise<InfraSource> { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); @@ -55,7 +55,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 +65,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, @@ -136,7 +136,10 @@ export class InfraSources { ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); + const { configuration, version } = await this.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -199,10 +202,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 ); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e3804078604cc5..d4dfa60ac67a0d 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -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 7e9b7ada28c8e1..687e368736a410 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 3a6bdaf3804e33..85dba8f598a89d 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 00000000000000..0ce594675773c2 --- /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, + sourceId, + patchedSourceConfigurationProperties + ) + : sources.createSourceConfiguration( + requestContext, + 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 00000000000000..5b68152b329efc --- /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 00000000000000..cdd053d2bb10ae --- /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 c45f191b1130d8..fe142aa93dcda0 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/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index 36906f6f4125bf..a457ccac2416c3 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 e45b9884967d0d..d1dc03893a0d9b 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -42,7 +42,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), diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 2f29320d7bb81d..62b7fd7ba902f5 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/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 9cd4821c2a727d..0e7abcc3d74a98 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -1,60 +1,70 @@ # Ingest Manager + ## Plugin - - 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. + +- 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 --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 0b462fb4c03195..f3990ba78c5391 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 337022e5522786..c5067480fb9538 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', diff --git a/x-pack/plugins/ingest_manager/common/constants/datasource.ts b/x-pack/plugins/ingest_manager/common/constants/datasource.ts index 0ff472b2afeb09..08113cff53bdae 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 f4a4bcde2f393d..fd28b6632b15ca 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 eb72c28e7bf399..4fb259609493dc 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 45d315e6d56649..6a2e559bbbe4fe 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 4c22d0e3fe7a30..ac2d6117be921e 100644 --- a/x-pack/plugins/ingest_manager/common/constants/output.ts +++ b/x-pack/plugins/ingest_manager/common/constants/output.ts @@ -5,7 +5,7 @@ */ 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', diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 98ca52651a2ae3..35e3be98e3982c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -48,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`, 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 00000000000000..a9e7f1df4119ca --- /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 7bbac55f119370..36e6e84b35bfe2 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 17509571f19853..a7d4e36d16f2ad 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 @@ -26,7 +26,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, - config: { + vars: { inputVar: { value: 'input-value' }, inputVar2: { value: undefined }, inputVar3: { @@ -40,11 +40,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'], }, @@ -53,7 +53,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-bar', enabled: true, dataset: 'bar', - config: { + vars: { barVar: { value: 'bar-value' }, barVar2: { value: [1, 2] }, barVar3: { 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 9e09d3fa3153a0..5deb33ccf10f1b 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 @@ -22,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 5fa7af2dda79ab..a977a1a66e059a 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => { name: 'mock-package', title: 'Mock package', version: '0.0.0', + latestVersion: '0.0.0', description: 'description', type: 'mock', categories: [], @@ -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 fa3479a69e39d9..e7a912ddf1741d 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 46b76d886f3cd3..1a1bd7c65aa258 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -13,6 +13,8 @@ import { AGENT_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, SETUP_API_ROUTE, + OUTPUT_API_ROUTES, + SETTINGS_API_ROUTES, } from '../constants'; export const epmRouteService = { @@ -112,6 +114,18 @@ export const agentRouteService = { 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 42f7a9333118e5..748bb14d2d35de 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/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 48243a12120f98..885e0a9316d793 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -23,7 +23,8 @@ export interface DatasourceInputStream { dataset: string; processors?: string[]; config?: DatasourceConfigRecord; - pkg_stream?: any; + vars?: DatasourceConfigRecord; + agent_stream?: any; } export interface DatasourceInput { @@ -31,6 +32,7 @@ export interface DatasourceInput { enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; + vars?: DatasourceConfigRecord; streams: DatasourceInputStream[]; } 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 c750aa99204fa9..f8779a879a049f 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', @@ -96,6 +97,7 @@ export interface RegistryStream { description?: string; enabled?: boolean; vars?: RegistryVarsEntry[]; + template?: string; } export type RequirementVersion = string; @@ -202,6 +204,8 @@ export interface RegistryVarsEntry { // internal until we need them interface PackageAdditions { title: string; + latestVersion: string; + installedVersion?: string; assets: AssetsGroupedByServiceByType; } @@ -246,6 +250,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', @@ -261,12 +266,17 @@ 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 { 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 f73ab7af636a90..2310fdd54a7198 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -11,3 +11,4 @@ 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 00000000000000..2921808230b47f --- /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/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index c1805023f497a8..763fb7d820b2aa 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 @@ -12,3 +12,5 @@ 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 00000000000000..41620603633818 --- /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 00000000000000..c02a5e5878ee91 --- /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 38f80a8bdc0222..fdb54411f86106 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/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index bdc8f350f7108b..b0b4e79cece79f 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 @@ -7,3 +7,4 @@ 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 45cf5c7e3ab15d..1c9bd9107515d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -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; 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 00000000000000..92146e9ee56793 --- /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 Management 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 619d03651dd961..e9b736e379b58e 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'; 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 4aa0ad7155d2f5..c535dc899638dc 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 d16d835f8c701c..bed3f994005ad2 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, @@ -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) => { 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 e4abb4ccd22cb5..10d9e03e986e10 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 084aba9a34309d..c39d2a5860bf00 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 @@ -10,3 +10,5 @@ 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 00000000000000..e57256d33ab2f4 --- /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 00000000000000..45e4eccf6625e1 --- /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 c63383637e792f..fbbc482fb96af2 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/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index f1f9063de72f00..10245e73520f75 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,10 +5,10 @@ */ 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 } from '../components'; +import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; @@ -35,59 +35,79 @@ 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> - <EuiTab isSelected={section === 'data_stream'} href={useLink(DATA_STREAM_PATH)}> + <> + {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}> + <EuiButtonEmpty iconType="gear" onClick={() => setIsSettingsFlyoutOpen(true)}> <FormattedMessage - id="xpack.ingestManager.appNavigation.dataStreamsLinkText" - defaultMessage="Data streams" + id="xpack.ingestManager.appNavigation.settingsButton" + defaultMessage="General settings" /> - </EuiTab> - </EuiTabs> - </EuiFlexItem> - </EuiFlexGroup> - </Nav> - {children} - <AlphaMessaging /> - </Container> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </Nav> + {children} + <AlphaMessaging /> + </Container> + </> ); }; 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 b18349e0787666..9ae8369abbd52e 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 @@ -8,6 +8,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks'; interface Props { @@ -122,7 +123,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 : (${agentConfigsToCheck.join(' or ')})`, }, }); setAgentsCount(data?.total || 0); 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 089b0631c20904..df679d33e03249 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/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 0e8763cb2d4c09..36e987d0076797 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 6b0c68ccb7d3fa..586fc6b1d41389 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 43e8f5a2c060db..7e32936a6fffa0 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/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 a45fabeb5ed6a3..b970a7d2220019 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 518e2bfc1af07a..3a712b072dac11 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/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 ad27c590d5eaae..f1d7bd5dbc0396 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 @@ -45,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 />; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx new file mode 100644 index 00000000000000..64223efefaab8c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +export const StyledAlert = styled(EuiIcon)` + color: ${props => props.theme.eui.euiColorWarning}; + padding: 0 5px; +`; + +export const UpdateIcon = () => <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 8ad081cbbabe40..ab7e87b3ad06cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -30,9 +30,15 @@ export function PackageCard({ showInstalledBadge, status, icons, + ...restProps }: PackageCardProps) { const { toDetailView } = useLinks(); - const url = toDetailView({ name, version }); + let urlVersion = version; + // if this is an installed package, link to the version installed + if ('savedObject' in restProps) { + urlVersion = restProps.savedObject.attributes.version || version; + } + const url = toDetailView({ name, version: urlVersion }); return ( <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 685199245df18f..54cb5171f5a3e3 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 0c5f45cdc47a71..244a9a2c7426e8 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 a3d24e7806f343..96aebb08e0c636 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,7 +50,7 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick<DetailParams, 'panel'>; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title, removable } = props; + const { panel, name, version, assets, title, removable, latestVersion } = props; switch (panel) { case 'settings': return ( @@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) { assets={assets} title={title} removable={removable} + latestVersion={latestVersion} /> ); case 'data-sources': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index fa3245aec02c5b..c82b7ed2297a7c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - if (packageInstallStatus !== InstallStatus.installed) + if (packageInstallStatus.status !== InstallStatus.installed) return ( <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 d83910f29f1a78..d20350c5db631e 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,14 @@ 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, installedVersion, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); - + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( <Fragment> <FullWidthNavRow> @@ -59,7 +54,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 3239d7b90e3c3c..1f3eb2cc9362e5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -32,11 +32,12 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; + const installedVersion = packageInfo?.installedVersion; const status: InstallStatus = packageInfo?.status as any; // track install status state if (name) { - setPackageInstallStatus({ name, status }); + setPackageInstallStatus({ name, status, version: installedVersion || null }); } if (packageInfo) { setInfo({ ...packageInfo, title: title || '' }); @@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick<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 cbbf1ce53c4ea3..cdad67fd875483 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick<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 f947466caf4b09..4d4dba2a64e5a6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,10 +8,22 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { 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 @@ -19,18 +31,37 @@ const NoteLabel = () => ( 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' | 'removable'> + 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, removable } = props; - const packageInstallStatus = getPackageInstallStatus(name); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; return ( <EuiText> <EuiTitle> @@ -42,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, }} @@ -57,80 +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 || removable === false ? true : packageHasDatasources} - /> - </p> - </EuiFlexItem> - </EuiFlexGroup> - {packageHasDatasources && removable === true && ( - <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> - <NoteLabel /> - </strong> - ), - }} - /> - </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> - )} </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 05729ccfc1af42..ab168ef1530bd6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { : p.theme.eui.euiFontWeightRegular}; `; // don't display Data Sources tab if the package is not installed - if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources') + if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources') return null; return ( diff --git a/x-pack/plugins/ingest_manager/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 c0f55a5a275fd2..9168669d132a04 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 653e2eb9a3a3bb..b69dd6bcf84319 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 @@ -71,7 +71,7 @@ export const AgentDetailSection: React.FunctionComponent<Props> = ({ agent }) => // Fetch AgentConfig information const { isLoading: isAgentConfigLoading, data: agentConfigData } = useGetOneAgentConfig( - agent.config_id as string + agent.config_id ); const items = [ 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 9c14a2e9dfed13..dd34e7260b27b4 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 a0244c601cd963..1d2f3bd155622f 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 89801bc6bee1ed..67930e51418b0c 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 8ce20a85e14b82..00000000000000 --- 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 009080a4da1860..00000000000000 --- 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 41c6b5912cd319..00000000000000 --- 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 19957e7827680b..00000000000000 --- 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.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 677c3069dc43bd..05264c157434e0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -40,7 +40,11 @@ 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', @@ -180,7 +184,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { 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 ')})`; } @@ -394,7 +398,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }); setSearch(newSearch); }} - fieldPrefix="agents" + fieldPrefix={AGENT_SAVED_OBJECT_TYPE} /> </EuiFlexItem> <EuiFlexItem grow={2}> 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 index 11a049047b7877..692c60cdce38c1 100644 --- 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 @@ -45,7 +45,7 @@ export const AgentReassignConfigFlyout: React.FunctionComponent<Props> = ({ onCl const agentConfigsRequest = useGetAgentConfigs(); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; - const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId as string); + const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; const [isSubmitting, setIsSubmitting] = useState(false); 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 7520f88215efe6..c11e3a49c7693e 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/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 53dbe295718c50..e4791cc816d04a 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 @@ -15,6 +15,8 @@ export { 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 0c1e688c1751e6..72fa23fce212c8 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 @@ -18,6 +18,7 @@ export { DatasourceInput, DatasourceInputStream, DatasourceConfigRecordEntry, + Output, DataStream, // API schemas - Agent Config GetAgentConfigsResponse, @@ -49,6 +50,14 @@ export { GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, GetOneEnrollmentAPIKeyResponse, + // API schemas - Outputs + GetOutputsResponse, + PutOutputRequest, + PutOutputResponse, + // API schemas - Settings + GetSettingsResponse, + PutSettingsRequest, + PutSettingsResponse, // EPM types AssetReference, AssetsGroupedByServiceByType, diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index b2e72fefe59978..75c14ffc8fa84a 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -19,7 +19,9 @@ export { 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, @@ -30,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 851a58f5adac28..951ff2337d8c72 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -22,7 +22,7 @@ export const config = { enabled: schema.boolean({ defaultValue: false }), epm: schema.object({ enabled: schema.boolean({ defaultValue: true }), - registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), + registryUrl: schema.maybe(schema.uri()), }), fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -43,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 55aea4b1a4cdd2..097825e0b69e1d 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -12,7 +12,7 @@ import { PluginInitializerContext, SavedObjectsServiceStart, } from 'kibana/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart, EncryptedSavedObjectsPluginSetup, @@ -29,7 +29,7 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, } from './constants'; -import { registerEncryptedSavedObjects } from './saved_objects'; +import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, registerDatasourceRoutes, @@ -39,11 +39,18 @@ import { registerAgentRoutes, registerEnrollmentApiKeyRoutes, registerInstallScriptRoutes, + registerOutputRoutes, + registerSettingsRoutes, } from './routes'; import { IngestManagerConfigType } from '../common'; -import { appContextService, ESIndexPatternSavedObjectService } from './services'; -import { ESIndexPatternService, AgentService } from './services'; +import { + appContextService, + licenseService, + ESIndexPatternSavedObjectService, + ESIndexPatternService, + AgentService, +} from './services'; import { getAgentStatusById } from './services/agents'; export interface IngestManagerSetupDeps { @@ -90,6 +97,7 @@ export class IngestManagerPlugin IngestManagerSetupDeps, IngestManagerStartDeps > { + private licensing$!: Observable<ILicense>; private config$: Observable<IngestManagerConfigType>; private security: SecurityPluginSetup | undefined; @@ -98,10 +106,12 @@ export class IngestManagerPlugin } 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 @@ -142,6 +152,8 @@ export class IngestManagerPlugin // Register routes registerAgentConfigRoutes(router); registerDatasourceRoutes(router); + registerOutputRoutes(router); + registerSettingsRoutes(router); registerDataStreamRoutes(router); // Conditional routes @@ -173,6 +185,7 @@ export class IngestManagerPlugin config$: this.config$, savedObjects: core.savedObjects, }); + licenseService.start(this.licensing$); return { esIndexPatternService: new ESIndexPatternSavedObjectService(), agentService: { @@ -183,5 +196,6 @@ export class IngestManagerPlugin public async stop() { appContextService.stop(); + licenseService.stop(); } } 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 67f758c2c12632..42298960cc615d 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,6 +8,7 @@ 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, @@ -50,7 +51,7 @@ export const getAgentConfigsHandler: RequestHandler< showInactive: true, 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 } ); 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 56d6053a1451b7..8f07e3ed1de022 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/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 8a186c54850245..3ce34d15de46c5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -11,3 +11,5 @@ 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 00000000000000..cd35b2a43426ca --- /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 00000000000000..139d11dba951a2 --- /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 00000000000000..56e666056e8d0a --- /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/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 95edb426ba72e3..37f142879aed51 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,157 +15,246 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_SAVED_OBJET_TYPE, } from './constants'; -import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; /* - * 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: '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' }, + 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' }, - data: { type: 'binary' }, - 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' }, + }, }, }, [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { - 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' }, + 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' }, - 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' }, - removable: { 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 ) { 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 309ddca3784c29..75bbfc21293c2b 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -67,7 +67,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 +244,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) { 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 a8fada00e25da6..ae0dedce178a81 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 @@ -14,7 +14,7 @@ import { 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'; @@ -31,7 +31,7 @@ describe('test agent acks services', () => { Promise.resolve({ id: 'action1', references: [], - type: 'agent_actions', + type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', agent_id: 'id', @@ -48,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', @@ -137,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/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index e37e6936d93eb9..43fd5a3ce0ac94 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})`); } 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 707229845531ce..2758374eba65f9 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/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index fce989cea32483..c4d4a8436e1479 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { getAgent, listAgents } from './crud'; -import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus, Agent } from '../../types'; import { @@ -72,8 +72,8 @@ export async function getAgentStatusForConfig( 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, }) ) @@ -91,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/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 948e518dff5b45..fd57e83d7421ec 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -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, 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 c9ead09b0908d5..1ac812c3380cd9 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 @@ -31,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, }); @@ -80,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) { 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 09c59998388d16..3682ae6d1167b9 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 f27252aaa9a84f..0a5ba43e40fba9 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; @@ -201,20 +200,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 +219,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 21de625532f038..db2e4fe4746406 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 5d9a6d409aa1a0..8254c0d8aaa378 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 5cf1f241a709fe..440060aff96169 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,850 +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": { - "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": { - "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": { - "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" + }, + "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" + } } - } - }, - "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": { - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } + "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" } - ], - "date_detection": false, - "properties": { - "system": { - "properties": { - "core": { - "properties": { - "id": { - "type": "long" - }, - "user": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "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" + } } - } - }, - "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" + } } } } @@ -853,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" + } } } } @@ -1597,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 4df626259ece76..6ef6f863753b55 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 @@ -18,7 +18,9 @@ export const installTemplates = async ( 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 @@ -41,7 +43,6 @@ export const installTemplates = async ( return []; }; -// this is temporary until we update the registry to use index templates v2 structure const installPreBuiltTemplates = async ( pkgName: string, pkgVersion: string, @@ -52,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 * @@ -113,10 +185,23 @@ 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); return { templateName, 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 1a73c9581a2de7..3679c577ee5713 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 @@ -259,3 +259,53 @@ test('tests processing object field with dynamic set to strict', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(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(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyMapping)); +}); + +test('tests processing object field with property, reverse order', () => { + const objectFieldWithPropertyReversedLiteralYml = ` +- name: a.b + type: keyword +- name: a + type: object + `; + const objectFieldWithPropertyReversedMapping = { + properties: { + a: { + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyReversedMapping)); +}); 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 46b6923962462e..9736f6d1cbd3cd 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 @@ -45,7 +45,7 @@ export function getTemplate( ): IndexTemplate { const template = getBaseTemplate(type, templateName, mappings); if (pipelineName) { - template.settings.index.default_pipeline = pipelineName; + template.template.settings.index.default_pipeline = pipelineName; } return template; } @@ -212,59 +212,61 @@ function getBaseTemplate( 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: { - // 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: {}, }; } @@ -322,7 +324,7 @@ const updateExistingIndex = async ({ callCluster: CallESAsCurrentUser; indexTemplate: IndexTemplate; }) => { - const { settings, mappings } = indexTemplate; + const { settings, mappings } = indexTemplate.template; // try to update the mappings first // for now we assume updates are compatible try { 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 e3aef6077dbc30..42989bb1e3ac91 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,35 @@ 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) + ); + }); }); 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 9c9843e0454ab5..edf7624d3f0d5b 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 @@ -108,7 +108,15 @@ function dedupFields(fields: Fields): Fields { return f.name === field.name; }); if (found) { - if (found.type === 'group' && field.type === 'group' && found.fields && field.fields) { + if ( + (found.type === 'group' || found.type === 'object') && + field.type === 'group' && + field.fields + ) { + if (!found.fields) { + found.fields = []; + } + found.type = 'group'; found.fields = dedupFields(found.fields.concat(field.fields)); } else { // only 'group' fields can be merged in this way diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index d76584225877c2..da8d79a04b97cd 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -67,9 +67,10 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise<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/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f3..c67cccd044bf5e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -43,6 +43,7 @@ export function createInstallableFrom<T>( ? { ...from, status: InstallationStatus.installed, + installedVersion: savedObject.attributes.version, savedObject, } : { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 06f3decdbbe6f2..8f51c4d78305c0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -106,7 +106,7 @@ export async function installPackage(options: { try { await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); } catch (err) { - // some assets may not exist if deleting during a failed update + // log these errors, some assets may not exist if deleted during a failed update } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 498796438c6c8e..befb4722b6504f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -74,7 +74,21 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { try { - await callCluster('indices.deleteTemplate', { name }); + 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}`); } @@ -107,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets( const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } }); - await Promise.all(deletePromises); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new Error('error deleting saved object asset'); + } } 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 a96afc5eb7fa52..8e9b920875617f 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 00000000000000..d92d6faf8472ef --- /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/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 1b0f174cc1a8e7..483661b9de9153 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -3,10 +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 { SavedObjectsClientContract } from 'kibana/server'; import { AgentStatus } from '../../common/types/models'; - -export { appContextService } from './app_context'; +import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; /** @@ -36,3 +36,8 @@ export interface AgentService { 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 aebb8188db0ccd..395c9af4a4ca21 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 00000000000000..f1c09746d9abd1 --- /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 167a24481aba5d..390e240841611e 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 6186a11d700112..27ed1de8499871 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -51,6 +51,8 @@ export { DefaultPackages, TemplateRef, IndexTemplateMappings, + Settings, + SettingsSOAttributes, } from '../../common'; export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; 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 c0cfee8f231c9d..e71016560f60c7 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/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index 42b607fa1c7157..6976dae38d5f1d 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 @@ -10,3 +10,5 @@ export * from './datasource'; 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/server/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts new file mode 100644 index 00000000000000..79a7c444dacdb9 --- /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 00000000000000..8b7500e4a9bd95 --- /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/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 5dfae097be8345..3a280fb045b060 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -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/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index e75e5fe763d6ae..8db00aba0e36d3 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 @@ -40,7 +40,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 +156,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -203,6 +204,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -237,15 +239,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 +275,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -279,7 +284,7 @@ describe('xy_expression', () => { Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 10000, + "minInterval": 1728000, } `); }); @@ -307,6 +312,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -350,6 +356,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -383,6 +390,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -398,6 +406,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -414,6 +423,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -430,6 +440,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -472,6 +483,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -510,6 +522,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -527,6 +540,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -547,6 +561,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -565,6 +580,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="CEST" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -582,6 +598,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -606,6 +623,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -624,6 +642,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -684,6 +703,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -878,6 +898,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -894,6 +915,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -910,6 +932,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -927,6 +950,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -943,6 +967,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} chartTheme={{}} + histogramBarTarget={50} timeZone="UTC" executeTriggerActions={executeTriggerActions} /> @@ -963,6 +988,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 d6b6de479acfb2..85cf5753befd79 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, @@ -35,8 +36,8 @@ import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { parseInterval } from '../../../../../src/plugins/data/common'; import { getExecuteTriggerActions } from './services'; +import { parseInterval } from '../../../../../src/plugins/data/common'; type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T; type SeriesSpec = InferPropType<typeof LineSeries> & @@ -58,6 +59,7 @@ type XYChartRenderProps = XYChartProps & { chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; + histogramBarTarget: number; executeTriggerActions: UiActionsStart['executeTriggerActions']; }; @@ -110,6 +112,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 +133,7 @@ export const getXyChartRenderer = (dependencies: { formatFactory={formatFactory} chartTheme={dependencies.chartTheme} timeZone={dependencies.timeZone} + histogramBarTarget={dependencies.histogramBarTarget} executeTriggerActions={executeTriggerActions} /> </I18nProvider>, @@ -169,6 +173,7 @@ export function XYChart({ formatFactory, timeZone, chartTheme, + histogramBarTarget, executeTriggerActions, }: XYChartRenderProps) { const { legend, layers } = args; @@ -212,18 +217,26 @@ 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() { + // add minInterval only for single row value as it cannot be determined from dataset + if (data.dateRange && layers.every(layer => data.tables[layer.layerId].rows.length <= 1)) { + 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() - : undefined; + 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 xDomain = data.dateRange && layers.every(l => l.xScaleType === 'time') ? { min: data.dateRange.fromDate.getTime(), max: data.dateRange.toDate.getTime(), - minInterval, + minInterval: calculateMinInterval(), } : undefined; return ( diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index d622e3f71eff54..154692a2fd1979 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 383245e6f4ee8a..ee43ac0ce233cd 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 00000000000000..5fbfbc3f577b81 --- /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 26b3bc6292dd62..2d073a92e507e9 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 00000000000000..f247c6ffcb526c --- /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 00000000000000..f0ef0dbec0b220 --- /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 00000000000000..47ffe3a3d9f54f --- /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 00000000000000..fc890dd3c927d6 --- /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 f46167a0d0a42f..f11d9d5e69a585 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 00000000000000..dbe31fed664131 --- /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 00000000000000..a05e97ded38eea --- /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 00000000000000..edc037ed7a0b12 --- /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 00000000000000..4a825382c06e49 --- /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 00000000000000..d70dd09849fa6c --- /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 00000000000000..596498b64b7713 --- /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 00000000000000..e0924392628a90 --- /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 00000000000000..e4cf46bc39429c --- /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 00000000000000..8f23f3744e563d --- /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 00000000000000..6fbc6ef2930647 --- /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 00000000000000..902d3e6a9896e4 --- /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 00000000000000..00a7c6f321d387 --- /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 00000000000000..6a60a6df55691a --- /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 00000000000000..8168e5a9838f20 --- /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 00000000000000..ba791a55d17eb8 --- /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 00000000000000..353a4ecdafa0ce --- /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 00000000000000..f4c1fb5c43eb0b --- /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 00000000000000..fd6aa5b85f81ae --- /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 00000000000000..14b201bf8089de --- /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 00000000000000..b8467d141bdd89 --- /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 00000000000000..0cf01db8617f0e --- /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 00000000000000..d332ab1eb1bab3 --- /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 00000000000000..e5d189db8490be --- /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 00000000000000..3e8198a5109b31 --- /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 00000000000000..efcb81fc8be2ac --- /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 00000000000000..9ea14a2a21ed8b --- /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 00000000000000..8803346709c313 --- /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 00000000000000..e1f88bae66e0f8 --- /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 00000000000000..d51ed60c41b56f --- /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 00000000000000..55aaf587ac06ba --- /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 00000000000000..3f11adf58d8d42 --- /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 00000000000000..bf2bf21d2c216e --- /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 00000000000000..6c2f2ed9a70952 --- /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 00000000000000..cad449766ceb4f --- /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 00000000000000..d1e2094bbcad3a --- /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 00000000000000..5e74753a6f0bde --- /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 00000000000000..b7aaac6d3fc760 --- /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 00000000000000..3e7995b2ce8d01 --- /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 00000000000000..3158fabda935f1 --- /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 00000000000000..f9bbbc4abad27e --- /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/index.ts b/x-pack/plugins/lists/server/index.ts new file mode 100644 index 00000000000000..c1e577aa601953 --- /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 00000000000000..4473d68d3c6465 --- /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 { ElasticsearchServiceSetup, 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'; + +export class ListPlugin { + private readonly logger: Logger; + private spaces: SpacesServiceSetup | undefined | null; + private config: ConfigType | undefined | null; + private elasticsearch: ElasticsearchServiceSetup | undefined | null; + private security: SecurityPluginSetup | undefined | null; + + constructor(private readonly initializerContext: PluginInitializerContext) { + 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.elasticsearch = core.elasticsearch; + 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, elasticsearch } = this; + const { + core: { + elasticsearch: { dataClient }, + }, + } = context; + if (config == null) { + throw new TypeError('Configuration is required for this plugin to operate'); + } else if (elasticsearch == null) { + throw new TypeError('Elastic Search is required for this plugin to operate'); + } else if (security == null) { + // TODO: This might be null, test authentication being turned off. + throw new TypeError('Security plugin is required for this plugin to operate'); + } else { + return { + getListClient: (): ListClient => + new ListClient({ + config, + dataClient, + request, + security, + spaces, + }), + }; + } + }; + }; +} 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 00000000000000..1c893fb757c5d0 --- /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 00000000000000..68622e98cbc526 --- /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 00000000000000..0f3c404c53cfd4 --- /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 00000000000000..424c3f45aac404 --- /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 00000000000000..51b4eb9f02cc26 --- /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 00000000000000..e89355b7689c5e --- /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 00000000000000..32b99bfc512bf2 --- /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 00000000000000..a3b6a520a4ecfc --- /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 00000000000000..4951cddc56939f --- /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 00000000000000..924dd086ee7080 --- /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 00000000000000..e18fd0618b133b --- /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 00000000000000..9d3fa4db8ccd05 --- /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 00000000000000..248fc72666d709 --- /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 00000000000000..0a60cba786f049 --- /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 00000000000000..c30eadfca0b650 --- /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 00000000000000..494d57b93b8e47 --- /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 00000000000000..6ace61e46a7802 --- /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 00000000000000..a16163ec0fa3a8 --- /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 00000000000000..a601bdfc003c53 --- /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 00000000000000..fb3bbbe0fad186 --- /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 00000000000000..5b65bb14414c70 --- /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 00000000000000..9934ce61c71073 --- /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 00000000000000..85f06ffbd66705 --- /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 00000000000000..ab14d8c8a80edf --- /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 00000000000000..6d3213ccb87934 --- /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 00000000000000..ba355854c77cc9 --- /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 00000000000000..5efad01e9a68ec --- /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 00000000000000..7f0e4e30622668 --- /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 00000000000000..31d26e195815fd --- /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 00000000000000..24ca27b0c949da --- /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 00000000000000..861928866369bc --- /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 00000000000000..a39409cd082677 --- /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 00000000000000..4ec55cb4c5f7b2 --- /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 00000000000000..aee32e3a4bd925 --- /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 00000000000000..cf8ebcacae5a1d --- /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 00000000000000..196b3b149ab829 --- /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 00000000000000..3e12ef1754f07d --- /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 00000000000000..1516fa5057e50e --- /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 00000000000000..9730c1b7523f19 --- /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 00000000000000..4a95a62b67c3e8 --- /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 00000000000000..e8f5fa7e38a06d --- /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 00000000000000..b736e7b96ad98e --- /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 00000000000000..00c3496e71b358 --- /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 00000000000000..1a57ab8b6a3b91 --- /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 00000000000000..00c3496e71b358 --- /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 00000000000000..936a070ede52cc --- /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 00000000000000..7dfbd5b1bada56 --- /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 00000000000000..3a517a52dbd21a --- /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 00000000000000..406b03dc6499ca --- /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 00000000000000..6aaffee0bc4b2e --- /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 00000000000000..b7c372d3947e37 --- /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 00000000000000..b55a60420674f3 --- /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 00000000000000..4d93544d568a8d --- /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 00000000000000..e3153bfd25b19a --- /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 00000000000000..946e1c240be31c --- /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/test_readable'; + +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 00000000000000..fd8fe7077fd58d --- /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 00000000000000..b2bca241c468cb --- /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 "callAsCurrentUser" 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.dataClient.callAsCurrentUser).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 00000000000000..da1e192bf2412d --- /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 { + IdOrUndefined, + IndexEsListItemSchema, + ListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; +import { DataClient } from '../../types'; +import { transformListItemToElasticQuery } from '../utils'; + +export interface CreateListItemOptions { + id: IdOrUndefined; + listId: string; + type: Type; + value: string; + dataClient: DataClient; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createListItem = async ({ + id, + listId, + type, + value, + dataClient, + 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 dataClient.callAsCurrentUser('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 00000000000000..9263b975b20e74 --- /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 "callAsCurrentUser" with body, index, and the bulk items', async () => { + const options = getCreateListItemBulkOptionsMock(); + await createListItemsBulk(options); + const firstRecord: IndexEsListItemSchema = getIndexESListItemMock(); + const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2); + [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; + expect(options.dataClient.callAsCurrentUser).toBeCalledWith('bulk', { + 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.dataClient.callAsCurrentUser).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 00000000000000..7100a5f8eaabca --- /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 { transformListItemToElasticQuery } from '../utils'; +import { DataClient } from '../../types'; +import { + CreateEsBulkTypeSchema, + IndexEsListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; + +export interface CreateListItemsBulkOptions { + listId: string; + type: Type; + value: string[]; + dataClient: DataClient; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string[]; +} + +export const createListItemsBulk = async ({ + listId, + type, + value, + dataClient, + 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 dataClient.callAsCurrentUser('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 00000000000000..795c579462b691 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.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 { LIST_ITEM_ID, LIST_ITEM_INDEX, getListItemResponseMock } from '../mocks'; +import { getDeleteListItemOptionsMock } from '../mocks/get_delete_list_item_options_mock'; + +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.dataClient.callAsCurrentUser).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 00000000000000..ffce2d3b2af816 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.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 { Id, ListItemSchema } from '../../../common/schemas'; +import { DataClient } from '../../types'; + +import { getListItem } from '.'; + +export interface DeleteListItemOptions { + id: Id; + dataClient: DataClient; + listItemIndex: string; +} + +export const deleteListItem = async ({ + id, + dataClient, + listItemIndex, +}: DeleteListItemOptions): Promise<ListItemSchema | null> => { + const listItem = await getListItem({ dataClient, id, listItemIndex }); + if (listItem == null) { + return null; + } else { + await dataClient.callAsCurrentUser('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 00000000000000..dee890445f9a3b --- /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.dataClient.callAsCurrentUser).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 00000000000000..f2f5ec3078e621 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.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 { ListItemArraySchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue } from '../utils'; +import { DataClient } from '../../types'; + +import { getListItemByValues } from './get_list_item_by_values'; + +export interface DeleteListItemByValueOptions { + listId: string; + type: Type; + value: string; + dataClient: DataClient; + listItemIndex: string; +} + +export const deleteListItemByValue = async ({ + listId, + value, + type, + dataClient, + listItemIndex, +}: DeleteListItemByValueOptions): Promise<ListItemArraySchema> => { + const listItems = await getListItemByValues({ + dataClient, + listId, + listItemIndex, + type, + value: [value], + }); + const values = listItems.map(listItem => listItem.value); + const filter = getQueryFilterFromTypeValue({ + listId, + type, + value: values, + }); + await dataClient.callAsCurrentUser('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 00000000000000..937993f1d8f713 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.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 { LIST_ID, LIST_INDEX, getDataClientMock, getListItemResponseMock } from '../mocks'; +import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; + +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 dataClient = getDataClientMock(data); + const list = await getListItem({ dataClient, 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 dataClient = getDataClientMock(data); + const list = await getListItem({ dataClient, 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 00000000000000..1c91b698016483 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.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 { SearchResponse } from 'elasticsearch'; + +import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; +import { DataClient } from '../../types'; +import { deriveTypeFromItem, transformElasticToListItem } from '../utils'; + +interface GetListItemOptions { + id: Id; + dataClient: DataClient; + listItemIndex: string; +} + +export const getListItem = async ({ + id, + dataClient, + listItemIndex, +}: GetListItemOptions): Promise<ListItemSchema | null> => { + const listItemES: SearchResponse<SearchEsListItemSchema> = await dataClient.callAsCurrentUser( + '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 00000000000000..d30b3c795550f1 --- /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 00000000000000..a6efcbc0d3ffb9 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_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 { ListItemArraySchema, Type } from '../../../common/schemas'; +import { DataClient } from '../../types'; + +import { getListItemByValues } from '.'; + +export interface GetListItemByValueOptions { + listId: string; + dataClient: DataClient; + listItemIndex: string; + type: Type; + value: string; +} + +export const getListItemByValue = async ({ + listId, + dataClient, + listItemIndex, + type, + value, +}: GetListItemByValueOptions): Promise<ListItemArraySchema> => + getListItemByValues({ + dataClient, + 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 00000000000000..55b170487d95a4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.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 { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2, getDataClientMock } from '../mocks'; +import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; + +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 dataClient = getDataClientMock(data); + const listItem = await getListItemByValues({ + dataClient, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], + }); + expect(listItem).toEqual([]); + }); + + test('Returns transformed list item if the data exists within ES', async () => { + const data = getSearchListItemMock(); + const dataClient = getDataClientMock(data); + const listItem = await getListItemByValues({ + dataClient, + 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 00000000000000..1e5c0b4a6655cc --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.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 { SearchResponse } from 'elasticsearch'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { DataClient } from '../../types'; +import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; + +export interface GetListItemByValuesOptions { + listId: string; + dataClient: DataClient; + listItemIndex: string; + type: Type; + value: string[]; +} + +export const getListItemByValues = async ({ + listId, + dataClient, + listItemIndex, + type, + value, +}: GetListItemByValuesOptions): Promise<ListItemArraySchema> => { + const response: SearchResponse<SearchEsListItemSchema> = await dataClient.callAsCurrentUser( + '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 00000000000000..0ea8320e966bdf --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.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 { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { getSpace } from '../utils'; + +import { getListItemIndex } from './get_list_item_index'; + +jest.mock('../utils', () => ({ + getSpace: jest.fn(), +})); + +describe('get_list_item_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list item index when there is a space', async () => { + ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); + const rawRequest = httpServerMock.createRawRequest({}); + const request = KibanaRequest.from(rawRequest); + const listIndex = getListItemIndex({ + listsItemsIndexName: 'lists-items-index', + request, + spaces: undefined, + }); + 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 00000000000000..c9f1bfd4d44e46 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; + +import { SpacesServiceSetup } from '../../../../spaces/server'; +import { getSpace } from '../utils'; + +interface GetListItemIndexOptions { + spaces: SpacesServiceSetup | undefined | null; + request: KibanaRequest; + listsItemsIndexName: string; +} + +export const getListItemIndex = ({ + spaces, + request, + listsItemsIndexName, +}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${getSpace({ request, spaces })}`; 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 00000000000000..9c85fa6ff0256e --- /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 00000000000000..95f4a09b406488 --- /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 00000000000000..ee1d83fabca31a --- /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 00000000000000..ca69c26df52b5a --- /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 00000000000000..a4c84f73e78960 --- /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 00000000000000..4ef4110bc0742a --- /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 00000000000000..ce4f8125d77af3 --- /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 { + Id, + ListItemSchema, + MetaOrUndefined, + UpdateEsListItemSchema, +} from '../../../common/schemas'; +import { transformListItemToElasticQuery } from '../utils'; +import { DataClient } from '../../types'; + +import { getListItem } from './get_list_item'; + +export interface UpdateListItemOptions { + id: Id; + value: string | null | undefined; + dataClient: DataClient; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateListItem = async ({ + id, + value, + dataClient, + listItemIndex, + user, + meta, + dateNow, +}: UpdateListItemOptions): Promise<ListItemSchema | null> => { + const updatedAt = dateNow ?? new Date().toISOString(); + const listItem = await getListItem({ dataClient, 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 dataClient.callAsCurrentUser('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 00000000000000..f064543f1ec937 --- /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 00000000000000..1fe1023e28ab9f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.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 { Readable } from 'stream'; + +import { MetaOrUndefined, Type } from '../../../common/schemas'; +import { DataClient } from '../../types'; + +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; + dataClient: DataClient; + listItemIndex: string; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export const importListItemsToStream = ({ + listId, + stream, + dataClient, + 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, + dataClient, + listId, + listItemIndex, + meta, + type, + user, + }); + }); + + readBuffer.on('close', () => { + resolve(); + }); + }); +}; + +export interface WriteBufferToItemsOptions { + listId: string; + dataClient: DataClient; + listItemIndex: string; + buffer: string[]; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export interface LinesResult { + linesProcessed: number; + duplicatesFound: number; +} + +export const writeBufferToItems = async ({ + listId, + dataClient, + listItemIndex, + buffer, + type, + user, + meta, +}: WriteBufferToItemsOptions): Promise<LinesResult> => { + const items = await getListItemByValues({ + dataClient, + 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({ + dataClient, + 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 00000000000000..63e9aeb61bad06 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -0,0 +1,290 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, + getDataClientMock, + getExportListItemsToStreamOptionsMock, + getResponseOptionsMock, + getWriteNextResponseOptions, + getWriteResponseHitsToStreamOptionsMock, +} from '../mocks'; +import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; + +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.dataClient = getDataClientMock(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.dataClient = getDataClientMock(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'; + + const jestCalls = jest.fn().mockResolvedValueOnce(firstResponse); + jestCalls.mockResolvedValueOnce(secondResponse); + + const dataClient = getDataClientMock(firstResponse); + dataClient.callAsCurrentUser = jestCalls; + options.dataClient = dataClient; + + 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.dataClient = getDataClientMock(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.dataClient = getDataClientMock(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.dataClient.callAsCurrentUser).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.dataClient.callAsCurrentUser).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 00000000000000..0e0ae7b924e17a --- /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 { SearchEsListItemSchema } from '../../../common/schemas'; +import { DataClient } from '../../types'; +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; + dataClient: DataClient; + listItemIndex: string; + stream: PassThrough; + stringToAppend: string | null | undefined; +} + +export const exportListItemsToStream = ({ + listId, + dataClient, + 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({ + dataClient, + listId, + listItemIndex, + searchAfter: undefined, + stream, + stringToAppend, + }); + while (searchAfter != null) { + searchAfter = await writeNextResponse({ + dataClient, + listId, + listItemIndex, + searchAfter, + stream, + stringToAppend, + }); + } + stream.end(); + }); +}; + +export interface WriteNextResponseOptions { + listId: string; + dataClient: DataClient; + listItemIndex: string; + stream: PassThrough; + searchAfter: string[] | undefined; + stringToAppend: string | null | undefined; +} + +export const writeNextResponse = async ({ + listId, + dataClient, + stream, + listItemIndex, + searchAfter, + stringToAppend, +}: WriteNextResponseOptions): Promise<string[] | undefined> => { + const response = await getResponse({ + dataClient, + 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 { + dataClient: DataClient; + listId: string; + searchAfter: undefined | string[]; + listItemIndex: string; + size?: number; +} + +export const getResponse = async ({ + dataClient, + searchAfter, + listId, + listItemIndex, + size = SIZE, +}: GetResponseOptions): Promise<SearchResponse<SearchEsListItemSchema>> => { + return dataClient.callAsCurrentUser('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 00000000000000..32578fc739f265 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client.ts @@ -0,0 +1,465 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, ScopedClusterClient } from 'src/core/server'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { SpacesServiceSetup } from '../../../../spaces/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 { getUser } from '../../services/utils'; +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'; + +// TODO: Consider an interface and a factory +export class ListClient { + private readonly spaces: SpacesServiceSetup | undefined | null; + private readonly config: ConfigType; + private readonly dataClient: Pick< + ScopedClusterClient, + 'callAsCurrentUser' | 'callAsInternalUser' + >; + private readonly request: KibanaRequest; + private readonly security: SecurityPluginSetup; + + constructor({ request, spaces, config, dataClient, security }: ConstructorOptions) { + this.request = request; + this.spaces = spaces; + this.config = config; + this.dataClient = dataClient; + this.security = security; + } + + public getListIndex = (): string => { + const { + spaces, + request, + config: { listIndex: listsIndexName }, + } = this; + return getListIndex({ listsIndexName, request, spaces }); + }; + + public getListItemIndex = (): string => { + const { + spaces, + request, + config: { listItemIndex: listsItemsIndexName }, + } = this; + return getListItemIndex({ listsItemsIndexName, request, spaces }); + }; + + public getList = async ({ id }: GetListOptions): Promise<ListSchema | null> => { + const { dataClient } = this; + const listIndex = this.getListIndex(); + return getList({ dataClient, id, listIndex }); + }; + + public createList = async ({ + id, + name, + description, + type, + meta, + }: CreateListOptions): Promise<ListSchema> => { + const { dataClient, security, request } = this; + const listIndex = this.getListIndex(); + const user = getUser({ request, security }); + return createList({ dataClient, 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 { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return getIndexExists(callAsCurrentUser, listIndex); + }; + + public getListItemIndexExists = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return getIndexExists(callAsCurrentUser, listItemIndex); + }; + + public createListBootStrapIndex = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return createBootstrapIndex(callAsCurrentUser, listIndex); + }; + + public createListItemBootStrapIndex = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return createBootstrapIndex(callAsCurrentUser, listItemIndex); + }; + + public getListPolicyExists = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return getPolicyExists(callAsCurrentUser, listIndex); + }; + + public getListItemPolicyExists = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listsItemIndex = this.getListItemIndex(); + return getPolicyExists(callAsCurrentUser, listsItemIndex); + }; + + public getListTemplateExists = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return getTemplateExists(callAsCurrentUser, listIndex); + }; + + public getListItemTemplateExists = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return getTemplateExists(callAsCurrentUser, 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 { + dataClient: { callAsCurrentUser }, + } = this; + const template = this.getListTemplate(); + const listIndex = this.getListIndex(); + return setTemplate(callAsCurrentUser, listIndex, template); + }; + + public setListItemTemplate = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const template = this.getListItemTemplate(); + const listItemIndex = this.getListItemIndex(); + return setTemplate(callAsCurrentUser, listItemIndex, template); + }; + + public setListPolicy = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return setPolicy(callAsCurrentUser, listIndex, listPolicy); + }; + + public setListItemPolicy = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return setPolicy(callAsCurrentUser, listItemIndex, listsItemsPolicy); + }; + + public deleteListIndex = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return deleteAllIndex(callAsCurrentUser, `${listIndex}-*`); + }; + + public deleteListItemIndex = async (): Promise<boolean> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return deleteAllIndex(callAsCurrentUser, `${listItemIndex}-*`); + }; + + public deleteListPolicy = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return deletePolicy(callAsCurrentUser, listIndex); + }; + + public deleteListItemPolicy = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return deletePolicy(callAsCurrentUser, listItemIndex); + }; + + public deleteListTemplate = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listIndex = this.getListIndex(); + return deleteTemplate(callAsCurrentUser, listIndex); + }; + + public deleteListItemTemplate = async (): Promise<unknown> => { + const { + dataClient: { callAsCurrentUser }, + } = this; + const listItemIndex = this.getListItemIndex(); + return deleteTemplate(callAsCurrentUser, listItemIndex); + }; + + public deleteListItem = async ({ id }: DeleteListItemOptions): Promise<ListItemSchema | null> => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItem({ dataClient, id, listItemIndex }); + }; + + public deleteListItemByValue = async ({ + listId, + value, + type, + }: DeleteListItemByValueOptions): Promise<ListItemArraySchema> => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItemByValue({ + dataClient, + listId, + listItemIndex, + type, + value, + }); + }; + + public deleteList = async ({ id }: DeleteListOptions): Promise<ListSchema | null> => { + const { dataClient } = this; + const listIndex = this.getListIndex(); + const listItemIndex = this.getListItemIndex(); + return deleteList({ + dataClient, + id, + listIndex, + listItemIndex, + }); + }; + + public exportListItemsToStream = ({ + stringToAppend, + listId, + stream, + }: ExportListItemsToStreamOptions): void => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + exportListItemsToStream({ + dataClient, + listId, + listItemIndex, + stream, + stringToAppend, + }); + }; + + public importListItemsToStream = async ({ + type, + listId, + stream, + meta, + }: ImportListItemsToStreamOptions): Promise<void> => { + const { dataClient, security, request } = this; + const listItemIndex = this.getListItemIndex(); + const user = getUser({ request, security }); + return importListItemsToStream({ + dataClient, + listId, + listItemIndex, + meta, + stream, + type, + user, + }); + }; + + public getListItemByValue = async ({ + listId, + value, + type, + }: GetListItemByValueOptions): Promise<ListItemArraySchema> => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValue({ + dataClient, + listId, + listItemIndex, + type, + value, + }); + }; + + public createListItem = async ({ + id, + listId, + value, + type, + meta, + }: CreateListItemOptions): Promise<ListItemSchema> => { + const { dataClient, security, request } = this; + const listItemIndex = this.getListItemIndex(); + const user = getUser({ request, security }); + return createListItem({ + dataClient, + id, + listId, + listItemIndex, + meta, + type, + user, + value, + }); + }; + + public updateListItem = async ({ + id, + value, + meta, + }: UpdateListItemOptions): Promise<ListItemSchema | null> => { + const { dataClient, security, request } = this; + const user = getUser({ request, security }); + const listItemIndex = this.getListItemIndex(); + return updateListItem({ + dataClient, + id, + listItemIndex, + meta, + user, + value, + }); + }; + + public updateList = async ({ + id, + name, + description, + meta, + }: UpdateListOptions): Promise<ListSchema | null> => { + const { dataClient, security, request } = this; + const user = getUser({ request, security }); + const listIndex = this.getListIndex(); + return updateList({ + dataClient, + description, + id, + listIndex, + meta, + name, + user, + }); + }; + + public getListItem = async ({ id }: GetListItemOptions): Promise<ListItemSchema | null> => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + return getListItem({ + dataClient, + id, + listItemIndex, + }); + }; + + public getListItemByValues = async ({ + type, + listId, + value, + }: GetListItemsByValueOptions): Promise<ListItemArraySchema> => { + const { dataClient } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValues({ + dataClient, + 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 00000000000000..c3b6a484d87876 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client_types.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 { PassThrough, Readable } from 'stream'; + +import { KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { SpacesServiceSetup } from '../../../../spaces/server'; +import { + Description, + DescriptionOrUndefined, + Id, + IdOrUndefined, + MetaOrUndefined, + Name, + NameOrUndefined, + Type, +} from '../../../common/schemas'; +import { ConfigType } from '../../config'; +import { DataClient } from '../../types'; + +export interface ConstructorOptions { + config: ConfigType; + dataClient: DataClient; + request: KibanaRequest; + spaces: SpacesServiceSetup | undefined | null; + security: SecurityPluginSetup; +} + +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 00000000000000..d6ba435155c60f --- /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 "callAsCurrentUser" 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.dataClient.callAsCurrentUser).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 00000000000000..dcf87b3ad1ef16 --- /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 { DataClient } from '../../types'; +import { + Description, + IdOrUndefined, + IndexEsListSchema, + ListSchema, + MetaOrUndefined, + Name, + Type, +} from '../../../common/schemas'; + +export interface CreateListOptions { + id: IdOrUndefined; + type: Type; + name: Name; + description: Description; + dataClient: DataClient; + listIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createList = async ({ + id, + name, + type, + description, + dataClient, + 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 dataClient.callAsCurrentUser('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 00000000000000..f32273e3e7f768 --- /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.dataClient.callAsCurrentUser).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.dataClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); + }); + + test('Delete does not call data client if the list returns null', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListOptionsMock(); + await deleteList(options); + expect(options.dataClient.callAsCurrentUser).not.toHaveBeenCalled(); + }); +}); 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 00000000000000..653a8da74a105d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/delete_list.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 { Id, ListSchema } from '../../../common/schemas'; +import { DataClient } from '../../types'; + +import { getList } from './get_list'; + +export interface DeleteListOptions { + id: Id; + dataClient: DataClient; + listIndex: string; + listItemIndex: string; +} + +export const deleteList = async ({ + id, + dataClient, + listIndex, + listItemIndex, +}: DeleteListOptions): Promise<ListSchema | null> => { + const list = await getList({ dataClient, id, listIndex }); + if (list == null) { + return null; + } else { + await dataClient.callAsCurrentUser('deleteByQuery', { + body: { + query: { + term: { + list_id: id, + }, + }, + }, + index: listItemIndex, + }); + + await dataClient.callAsCurrentUser('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 00000000000000..1f9a33c191764f --- /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, + getDataClientMock, + 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 dataClient = getDataClientMock(data); + const list = await getList({ dataClient, 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 dataClient = getDataClientMock(data); + const list = await getList({ dataClient, 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 00000000000000..216703f08f0697 --- /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 { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; +import { DataClient } from '../../types'; + +interface GetListOptions { + id: Id; + dataClient: DataClient; + listIndex: string; +} + +export const getList = async ({ + id, + dataClient, + listIndex, +}: GetListOptions): Promise<ListSchema | null> => { + const result: SearchResponse<SearchEsListSchema> = await dataClient.callAsCurrentUser('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 00000000000000..22a738a340b25d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.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 { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { getSpace } from '../utils'; + +import { getListIndex } from './get_list_index'; + +jest.mock('../utils', () => ({ + getSpace: jest.fn(), +})); + +describe('get_list_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list index when there is a space', async () => { + ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); + const rawRequest = httpServerMock.createRawRequest({}); + const request = KibanaRequest.from(rawRequest); + const listIndex = getListIndex({ + listsIndexName: 'lists-index', + request, + spaces: undefined, + }); + 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 00000000000000..70b85fc97ebfa1 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_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 { KibanaRequest } from 'kibana/server'; + +import { SpacesServiceSetup } from '../../../../spaces/server'; +import { getSpace } from '../utils'; + +interface GetListIndexOptions { + spaces: SpacesServiceSetup | undefined | null; + request: KibanaRequest; + listsIndexName: string; +} + +export const getListIndex = ({ spaces, request, listsIndexName }: GetListIndexOptions): string => + `${listsIndexName}-${getSpace({ request, spaces })}`; 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 00000000000000..e25eaaafd855e9 --- /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 00000000000000..9d93a051f2d108 --- /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 00000000000000..f704ef0b05b82f --- /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 00000000000000..1136a53da787d2 --- /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 00000000000000..a4c84f73e78960 --- /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 00000000000000..09bf0ee69c981f --- /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 00000000000000..55f110e9a8291f --- /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 { + DescriptionOrUndefined, + Id, + ListSchema, + MetaOrUndefined, + NameOrUndefined, + UpdateEsListSchema, +} from '../../../common/schemas'; +import { DataClient } from '../../types'; + +import { getList } from '.'; + +export interface UpdateListOptions { + id: Id; + dataClient: DataClient; + listIndex: string; + user: string; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateList = async ({ + id, + name, + description, + dataClient, + listIndex, + user, + meta, + dateNow, +}: UpdateListOptions): Promise<ListSchema | null> => { + const updatedAt = dateNow ?? new Date().toISOString(); + const list = await getList({ dataClient, id, listIndex }); + if (list == null) { + return null; + } else { + const doc: UpdateEsListSchema = { + description, + meta, + name, + updated_at: updatedAt, + updated_by: user, + }; + const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('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_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 00000000000000..0f4d92cabaa7a1 --- /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 { getDataClientMock } from './get_data_client_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 => ({ + dataClient: getDataClientMock(), + 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 00000000000000..960db293f11245 --- /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 { getDataClientMock } from './get_data_client_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 => ({ + dataClient: getDataClientMock(), + 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 00000000000000..1a005a76547f5c --- /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 { getDataClientMock } from './get_data_client_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getCreateListOptionsMock = (): CreateListOptions => ({ + dataClient: getDataClientMock(), + 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_data_client_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts new file mode 100644 index 00000000000000..6e4cc40efeed73 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.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 { CreateDocumentResponse } from 'elasticsearch'; + +import { LIST_INDEX } from './lists_services_mock_constants'; +import { getShardMock } from './get_shard_mock'; + +interface DataClientReturn { + callAsCurrentUser: () => Promise<unknown>; + callAsInternalUser: () => Promise<never>; +} + +export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ + _id: 'elastic-id-123', + _index: LIST_INDEX, + _shards: getShardMock(), + _type: '', + _version: 1, + created: true, + result: '', +}); + +export const getDataClientMock = ( + callAsCurrentUserData: unknown = getEmptyCreateDocumentResponseMock() +): DataClientReturn => ({ + callAsCurrentUser: jest.fn().mockResolvedValue(callAsCurrentUserData), + callAsInternalUser: (): Promise<never> => { + throw new Error('This function should not be calling "callAsInternalUser"'); + }, +}); 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 00000000000000..58fd319589ea3d --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..1e7167547a6de9 --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..9d70dae969362d --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListOptionsMock = (): DeleteListOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..4cc6d85cd947ab --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; +import { TestReadable } from './test_readable'; + +export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..574e4afcb36f05 --- /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 00000000000000..4e4d8d9c572e44 --- /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 00000000000000..ab1bde48e7ebf9 --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..c15d417d10289c --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants'; + +export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..1a30282ddaebac --- /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 00000000000000..ea068d774c4edc --- /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 00000000000000..5e9fd8995c0eb6 --- /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 00000000000000..6a565437617ba8 --- /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 00000000000000..9f877c8168cca9 --- /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 00000000000000..9728139eab42ad --- /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 00000000000000..4cc6577d5e5312 --- /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 00000000000000..b60d6f5113e06b --- /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 { getDataClientMock } from './get_data_client_mock'; +import { + DATE_NOW, + LIST_ITEM_ID, + LIST_ITEM_INDEX, + META, + USER, + VALUE, +} from './lists_services_mock_constants'; + +export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..e56ebc24bdae1e --- /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 { getDataClientMock } from './get_data_client_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + USER, +} from './lists_services_mock_constants'; + +export const getUpdateListOptionsMock = (): UpdateListOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..9a77453b65d6aa --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; + +export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ + buffer: [], + dataClient: getDataClientMock(), + 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 00000000000000..96724c2a88045c --- /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 { getDataClientMock } from './get_data_client_mock'; +import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; +import { getSearchListItemMock } from './get_search_list_item_mock'; + +export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ + dataClient: getDataClientMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ + dataClient: getDataClientMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + searchAfter: [], + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getResponseOptionsMock = (): GetResponseOptions => ({ + dataClient: getDataClientMock(), + 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 00000000000000..516264149fac70 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/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. + */ + +export * from './get_data_client_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'; 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 00000000000000..d174211f348ea5 --- /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 00000000000000..52ad6de4840058 --- /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 00000000000000..6e5dca7d54e5b8 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.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 { getSearchEsListItemMock } from '../mocks'; +import { Type } from '../../../common/schemas'; + +import { deriveTypeFromItem } from './derive_type_from_es_type'; + +describe('derive_type_from_es_type', () => { + 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 00000000000000..7a65e74bf49474 --- /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.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts new file mode 100644 index 00000000000000..3f50efe0c6c56b --- /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/get_space.ts b/x-pack/plugins/lists/server/services/utils/get_space.ts new file mode 100644 index 00000000000000..e23f963b2c40d2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_space.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 getSpace = ({ + spaces, + request, +}: { + spaces: SpacesServiceSetup | undefined | null; + request: KibanaRequest; +}): string => spaces?.getSpaceId(request) ?? 'default'; diff --git a/x-pack/plugins/lists/server/services/utils/get_user.ts b/x-pack/plugins/lists/server/services/utils/get_user.ts new file mode 100644 index 00000000000000..1ddad047da7229 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_user.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 { KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../../../security/server'; + +interface GetUserOptions { + security: SecurityPluginSetup; + request: KibanaRequest; +} + +export const getUser = ({ security, request }: GetUserOptions): string => { + const authenticatedUser = security.authc.getCurrentUser(request); + if (authenticatedUser != null) { + return authenticatedUser.username; + } else { + return 'elastic'; + } +}; 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 00000000000000..f256b6cb8f2d50 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/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 './get_query_filter_from_type_value'; +export * from './transform_elastic_to_list_item'; +export * from './transform_list_item_to_elastic_query'; +export * from './get_user'; +export * from './derive_type_from_es_type'; +export * from './get_space'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts new file mode 100644 index 00000000000000..9cf673081d3206 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.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 { SearchResponse } from 'elasticsearch'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export const transformElasticToListItem = ({ + response, + type, +}: { + response: SearchResponse<SearchEsListItemSchema>; + type: Type; +}): 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, + }; + } + } + // TypeScript is not happy unless I have this line here + return assertUnreachable(); + }); +}; + +export 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.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts new file mode 100644 index 00000000000000..e68851dc3582b7 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.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 { EsDataTypeUnion, Type } from '../../../common/schemas'; + +export const transformListItemToElasticQuery = ({ + type, + value, +}: { + type: Type; + value: string; + // We disable the consistent return since we want to use typescript for exhaustive type checks + // eslint-disable-next-line consistent-return +}): EsDataTypeUnion => { + switch (type) { + case 'ip': { + return { + ip: value, + }; + } + case 'keyword': { + return { + keyword: value, + }; + } + } +}; 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 00000000000000..e78debc8e43492 --- /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 00000000000000..7d509c4e271675 --- /dev/null +++ b/x-pack/plugins/lists/server/types.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 { IContextProvider, RequestHandler, ScopedClusterClient } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; + +import { ListClient } from './services/lists/client'; + +export type DataClient = Pick<ScopedClusterClient, 'callAsCurrentUser' | 'callAsInternalUser'>; +export type ContextProvider = IContextProvider<RequestHandler<unknown, unknown, unknown>, 'lists'>; + +export interface PluginsSetup { + security: SecurityPluginSetup; + 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/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index c8db284a5c4f1a..38c56405787eb2 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -74,3 +74,11 @@ 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/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js index 1eb5dac309f28c..f02ded1704533f 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -5,27 +5,18 @@ */ 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/es_pew_pew_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 = []) { @@ -35,18 +26,18 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { 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 layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: KibanaTilemapSource.createDescriptor(), + }); + return [layerDescriptor, ...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]; + const layerDescriptor = VectorTileLayer.createDescriptor({ + sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), + }); + return [layerDescriptor, ...initialLayers]; } return initialLayers; diff --git a/x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap deleted file mode 100644 index af836ceffa4b7e..00000000000000 --- a/x-pack/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/plugins/maps/public/components/layer_toc_actions.js b/x-pack/plugins/maps/public/components/layer_toc_actions.js deleted file mode 100644 index d79eda16037cb7..00000000000000 --- a/x-pack/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/plugins/maps/public/components/layer_toc_actions.test.js b/x-pack/plugins/maps/public/components/layer_toc_actions.test.js deleted file mode 100644 index c3a8f59c4c736a..00000000000000 --- a/x-pack/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/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 index a4fa0d492bf3f5..8ebb17ac4fff56 100644 --- 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 @@ -4,51 +4,36 @@ * 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'; +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; import { uploadLayerWizardConfig } from '../../../layers/sources/client_file_source'; -export const ImportEditor = ({ clearSource, isIndexingTriggered, ...props }) => { - const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); +export const ImportEditor = props => { + const editorProperties = getEditorProperties(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> + <EuiPanel style={{ position: 'relative' }}> + {uploadLayerWizardConfig.renderWizard(editorProperties)} + </EuiPanel> ); }; function getEditorProperties({ - inspectorAdapters, + previewLayer, + mapColors, onRemove, - viewLayer, isIndexingTriggered, onIndexReady, importSuccessHandler, importErrorHandler, }) { return { - onPreviewSource: viewLayer, - inspectorAdapters, + previewLayer, + mapColors, onRemove, importSuccessHandler, importErrorHandler, isIndexingTriggered, - addAndViewSource: viewLayer, 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 index 24c1f5ced4fe65..a29898f8a2830b 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js @@ -34,12 +34,12 @@ function mapStateToProps(state = {}) { function mapDispatchToProps(dispatch) { return { - viewLayer: async layer => { + previewLayer: async layerDescriptor => { await dispatch(setSelectedLayer(null)); await dispatch(removeTransientLayer()); - dispatch(addLayer(layer.toLayerDescriptor())); - dispatch(setSelectedLayer(layer.getId())); - dispatch(setTransientLayer(layer.getId())); + dispatch(addLayer(layerDescriptor)); + dispatch(setSelectedLayer(layerDescriptor.id)); + dispatch(setTransientLayer(layerDescriptor.id)); }, removeTransientLayer: () => { dispatch(setSelectedLayer(null)); diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js deleted file mode 100644 index 8937f32d3bf05d..00000000000000 --- a/x-pack/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'; - -import { getInspectorAdapters } from '../../../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/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js deleted file mode 100644 index 50312b68277fab..00000000000000 --- a/x-pack/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/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 index 80b05a0fd015b7..82df9237e6ed30 100644 --- 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 @@ -7,8 +7,7 @@ import React, { Fragment } from 'react'; import { getLayerWizards } from '../../../layers/layer_wizard_registry'; -import { EuiTitle, EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; import _ from 'lodash'; export function SourceSelect({ updateSourceSelection }) { @@ -38,17 +37,5 @@ export function SourceSelect({ updateSourceSelection }) { ); }); - return ( - <Fragment> - <EuiTitle size="xs"> - <h2> - <FormattedMessage - id="xpack.maps.addLayerPanel.chooseDataSourceTitle" - defaultMessage="Choose data source" - /> - </h2> - </EuiTitle> - {sourceCards} - </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 index 92fcf01f3901f5..127b99d730db57 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js @@ -4,18 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } 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 { 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, - layer: null, + layerDescriptor: null, // TODO get this from redux store instead of storing locally + isIndexingSource: false, importView: false, layerImportAddReady: false, }; @@ -35,13 +36,9 @@ export class AddLayerPanel extends Component { } _getPanelDescription() { - const { layerWizard, importView, layerImportAddReady } = this.state; + const { importView, layerImportAddReady } = this.state; let panelDescription; - if (!layerWizard) { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.selectSource', { - defaultMessage: 'Select source', - }); - } else if (layerImportAddReady || !importView) { + if (layerImportAddReady || !importView) { panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', { defaultMessage: 'Add layer', }); @@ -53,29 +50,21 @@ export class AddLayerPanel extends Component { return panelDescription; } - _viewLayer = async (source, options = {}) => { + _previewLayer = async (layerDescriptor, isIndexingSource) => { if (!this._isMounted) { return; } - if (!source) { - this.setState({ layer: null }); + if (!layerDescriptor) { + this.setState({ + layerDescriptor: null, + isIndexingSource: false, + }); 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)); + this.setState({ layerDescriptor, isIndexingSource }); + this.props.previewLayer(layerDescriptor); }; _clearLayerData = ({ keepSourceType = false }) => { @@ -84,7 +73,8 @@ export class AddLayerPanel extends Component { } this.setState({ - layer: null, + layerDescriptor: null, + isIndexingSource: false, ...(!keepSourceType ? { layerWizard: null, importView: false } : {}), }); this.props.removeTransientLayer(); @@ -95,72 +85,75 @@ export class AddLayerPanel extends Component { }; _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(); + if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { + this.props.setIndexingTriggered(); } else { - selectLayerAndAdd(); + this.props.selectLayerAndAdd(); if (this.state.importView) { this.setState({ layerImportAddReady: false, }); - resetIndexing(); + this.props.resetIndexing(); } } }; - _renderAddLayerPanel() { - const { layerWizard, importView } = this.state; - if (!layerWizard) { + _renderPanelBody() { + if (!this.state.layerWizard) { return <SourceSelect updateSourceSelection={this._onSourceSelectionChange} />; } - if (importView) { + + 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 ( - <ImportEditor - clearSource={this._clearLayerData} - viewLayer={this._viewLayer} - onRemove={() => this._clearLayerData({ keepSourceType: true })} - /> + <Fragment> + {backButton} + <ImportEditor + clearSource={this._clearLayerData} + previewLayer={this._previewLayer} + mapColors={this.props.mapColors} + onRemove={() => this._clearLayerData({ keepSourceType: true })} + /> + </Fragment> ); } - 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} - /> + <Fragment> + {backButton} + <EuiPanel> + {this.state.layerWizard.renderWizard({ + previewLayer: this._previewLayer, + mapColors: this.props.mapColors, + })} + </EuiPanel> + </Fragment> ); } - _renderFlyout() { + 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 ( - <EuiFlexGroup direction="column" gutterSize="none"> + <Fragment> <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> <EuiTitle size="s"> <h2>{panelDescription}</h2> @@ -168,14 +161,16 @@ export class AddLayerPanel extends Component { </EuiFlyoutHeader> <div className="mapLayerPanel__body" data-test-subj="layerAddForm"> - <div className="mapLayerPanel__bodyOverflow">{this._renderAddLayerPanel()}</div> + <div className="mapLayerPanel__bodyOverflow">{this._renderPanelBody()}</div> </div> - {this._renderFooter(panelDescription)} - </EuiFlexGroup> - ); - } - render() { - return this.props.flyoutVisible ? this._renderFlyout() : null; + <FlyoutFooter + showNextButton={!!this.state.layerWizard} + disableNextButton={!isNextBtnEnabled} + onClick={this._layerAddHandler} + nextButtonText={panelDescription} + /> + </Fragment> + ); } } 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 index 27ea52bfed0440..f1cb1a88647539 100644 --- 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 @@ -9,12 +9,10 @@ exports[`TOCEntry is rendered 1`] = ` <div className="mapTocEntry-visible" > - <LayerTocActions - cloneLayer={[Function]} + <Connect(TOCEntryActionsPopover) displayName="layer 1" editLayer={[Function]} escapedDisplayName="layer_1" - fitToBounds={[Function]} layer={ Object { "getDisplayName": [Function], @@ -26,9 +24,6 @@ exports[`TOCEntry is rendered 1`] = ` "showAtZoomLevel": [Function], } } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} /> <div className="mapTocEntry__layerIcons" @@ -76,12 +71,10 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = ` <div className="mapTocEntry-visible" > - <LayerTocActions - cloneLayer={[Function]} + <Connect(TOCEntryActionsPopover) displayName="layer 1" editLayer={[Function]} escapedDisplayName="layer_1" - fitToBounds={[Function]} layer={ Object { "getDisplayName": [Function], @@ -93,9 +86,6 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = ` "showAtZoomLevel": [Function], } } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} /> <div className="mapTocEntry__layerIcons" @@ -143,12 +133,10 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = ` <div className="mapTocEntry-visible" > - <LayerTocActions - cloneLayer={[Function]} + <Connect(TOCEntryActionsPopover) displayName="layer 1" editLayer={[Function]} escapedDisplayName="layer_1" - fitToBounds={[Function]} layer={ Object { "getDisplayName": [Function], @@ -160,9 +148,6 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = ` "showAtZoomLevel": [Function], } } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} /> <div className="mapTocEntry__layerIcons" @@ -210,13 +195,10 @@ exports[`TOCEntry props isReadOnly 1`] = ` <div className="mapTocEntry-visible" > - <LayerTocActions - cloneLayer={[Function]} + <Connect(TOCEntryActionsPopover) displayName="layer 1" editLayer={[Function]} escapedDisplayName="layer_1" - fitToBounds={[Function]} - isReadOnly={true} layer={ Object { "getDisplayName": [Function], @@ -228,9 +210,6 @@ exports[`TOCEntry props isReadOnly 1`] = ` "showAtZoomLevel": [Function], } } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} /> </div> <span @@ -261,12 +240,10 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is <div className="mapTocEntry-visible" > - <LayerTocActions - cloneLayer={[Function]} + <Connect(TOCEntryActionsPopover) displayName="layer 1" editLayer={[Function]} escapedDisplayName="layer_1" - fitToBounds={[Function]} layer={ Object { "getDisplayName": [Function], @@ -278,9 +255,6 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is "showAtZoomLevel": [Function], } } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} /> <div className="mapTocEntry__layerIcons" 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 index ca3c6d325687dd..b0d4d9d357988b 100644 --- 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 @@ -4,36 +4,32 @@ * 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'; - import { FLYOUT_STATE } from '../../../../../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 { + getMapZoom, hasDirtyState, getSelectedLayer, - isUsingSearch, } 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: _.get(state, 'map.mapState.zoom', 0), + zoom: getMapZoom(state), selectedLayer: getSelectedLayer(state), hasDirtyStateSelector: hasDirtyState(state), isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()), - isUsingSearch: isUsingSearch(state), + isEditButtonDisabled: + flyoutDisplay !== FLYOUT_STATE.NONE && flyoutDisplay !== FLYOUT_STATE.LAYER_PANEL, }; } @@ -44,18 +40,6 @@ function mapDispatchToProps(dispatch) { 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)); }, 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 00000000000000..b8c652909408ac --- /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 00000000000000..1437370557efc4 --- /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 00000000000000..b873119fd7d139 --- /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 00000000000000..d628cca61de113 --- /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 index fe56523fb25807..c0ce24fef9cd86 100644 --- 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 @@ -8,7 +8,7 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { LayerTocActions } from '../../../../../components/layer_toc_actions'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; import { i18n } from '@kbn/i18n'; function escapeLayerName(name) { @@ -124,6 +124,7 @@ export class TOCEntry extends React.Component { return ( <div className="mapTocEntry__layerIcons"> <EuiButtonIcon + isDisabled={this.props.isEditButtonDisabled} iconType="pencil" aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonAriaLabel', { defaultMessage: 'Edit layer', @@ -191,17 +192,7 @@ export class TOCEntry extends React.Component { } _renderLayerHeader() { - const { - removeLayer, - cloneLayer, - isReadOnly, - layer, - zoom, - toggleVisible, - fitToBounds, - isUsingSearch, - } = this.props; - + const { layer, zoom } = this.props; return ( <div className={ @@ -210,26 +201,12 @@ export class TOCEntry extends React.Component { : 'mapTocEntry-notVisible' } > - <LayerTocActions + <TOCEntryActionsPopover 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()); - }} + isEditButtonDisabled={this.props.isEditButtonDisabled} /> {this._renderLayerIcons()} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index be6c3bf978d82d..abfaba80c33d11 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -23,6 +23,7 @@ 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 '../index.scss'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; 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 1fc3ad203706f0..5c486200977d7c 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts @@ -156,7 +156,7 @@ 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); diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx index ce48793e1481bd..dccf413b489f1b 100644 --- a/x-pack/plugins/maps/public/layers/layer.tsx +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -45,7 +45,7 @@ export interface ILayer { supportsFitToBounds(): Promise<boolean>; getAttributions(): Promise<Attribution[]>; getLabel(): string; - getCustomIconAndTooltipContent(): IconAndTooltipContent; + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; renderLegendDetails(): ReactElement<any> | null; showAtZoomLevel(zoom: number): boolean; @@ -62,7 +62,6 @@ export interface ILayer { isLayerLoading(): boolean; hasErrors(): boolean; getErrors(): string; - toLayerDescriptor(): LayerDescriptor; getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; @@ -88,7 +87,11 @@ export type Footnote = { export type IconAndTooltipContent = { icon?: ReactElement<any> | null; tooltipContent?: string | null; - footnotes?: Footnote[] | null; + footnotes: Footnote[]; +}; +export type CustomIconAndTooltipContent = { + icon: ReactElement<any> | null; + tooltipContent?: string | null; areResultsTrimmed?: boolean; }; @@ -213,7 +216,7 @@ export class AbstractLayer implements ILayer { return this._descriptor.label ? this._descriptor.label : ''; } - getCustomIconAndTooltipContent(): IconAndTooltipContent { + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { return { icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />, }; @@ -413,10 +416,6 @@ export class AbstractLayer implements ILayer { : ''; } - toLayerDescriptor(): LayerDescriptor { - return this._descriptor; - } - async syncData(syncContext: SyncContext) { // no-op by default } 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 cb87aeaa9da3f2..7715541b1c52d7 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 49d128257fe206..0b83f3bbdc6133 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 36f898f7237574..3c9c71d2a1875c 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,17 +5,7 @@ */ 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 { @@ -66,103 +56,9 @@ export class GeojsonFileSource extends AbstractVectorSource { canFormatFeatureProperties() { return true; } - - shouldBeIndexed() { - return true; - } } -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 a6a31def4b2319..5c2a0afd318850 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 00000000000000..2f8aa67d74b52f --- /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 00000000000000..a6e2e7f42657c3 --- /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 e8af17b9119391..5802a223e48464 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 28fbc04a1a032a..e9bf592c6d2b7f 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 00000000000000..fc745edbabee87 --- /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 79121c4cdb31f6..3bed9b2c09570a 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 60a4c9b1de8914..704bcfd370a853 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 00000000000000..f9092e64833f13 --- /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.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index b9ef13e520bf89..17fad2f2e64534 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 00000000000000..fee1a81a5c63ab --- /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 c2fa2356b1a3e1..2db66ff4116276 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 57e5afb99404b5..0d15cff032410c 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 00000000000000..fabde578085ab3 --- /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 00000000000000..3ad6d64903d4aa --- /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 00000000000000..4a775dd78f7873 --- /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 c904280a38c85d..23e3c759d73c39 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 bfbcca1eb3f614..a412c49faceacb 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 { getSearchService } from '../../../kibana_services'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../vector_layer'; 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, @@ -586,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 66cc2ddd854041..00000000000000 --- 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 2c401ac92567e2..6ae327a18b7c2d 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.js b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js index ccd8bc4a859dbc..87733e347aa2a3 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 @@ -18,7 +18,6 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; 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'; @@ -176,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, }; @@ -223,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; } 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 00c3bfc5f17c62..6e9d7ad1a613b0 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 00000000000000..a9adec2bda2c81 --- /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 be333f8ee85a43..fb5a2e4f42f1d4 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 9fd7f088032ca4..cc890916054569 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 00000000000000..141fabeedd3e51 --- /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 bbb653eff32e24..7dc1d454a1c527 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 dfdea1489d50c2..c94fec3deac67e 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 a73cfbdc0d0435..58e6e39aaa1f96 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'; @@ -17,11 +16,10 @@ import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters import { MapExtent, TiledSingleLayerVectorSourceDescriptor, - VectorLayerDescriptor, 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,22 +64,6 @@ export class MVTSingleLayerVectorSource extends AbstractSource return []; } - createDefaultLayer(options?: Partial<VectorLayerDescriptor>): TiledVectorLayer { - const layerDescriptor: Partial<VectorLayerDescriptor> = { - sourceDescriptor: this._descriptor, - ...options, - }; - const normalizedLayerDescriptor: VectorLayerDescriptor = 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.ts b/x-pack/plugins/maps/public/layers/sources/source.ts index 1cd84010159abf..af934d7464f618 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.ts +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -13,8 +13,7 @@ import { Adapters } from 'src/plugins/inspector/public'; // @ts-ignore import { copyPersistentState } from '../../reducers/util'; -import { LayerDescriptor, SourceDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; +import { SourceDescriptor } from '../../../common/descriptor_types'; import { IField } from '../fields/field'; import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; @@ -37,7 +36,6 @@ export type PreIndexedShape = { export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; export interface ISource { - createDefaultLayer(options?: Partial<LayerDescriptor>): ILayer; destroy(): void; getDisplayName(): Promise<string>; getInspectorAdapters(): Adapters | undefined; @@ -59,7 +57,6 @@ export interface ISource { getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; getGeoGridPrecision(zoom: number): number; - shouldBeIndexed(): boolean; getPreIndexedShape(): Promise<PreIndexedShape | null>; createFieldFormatter(field: IField): Promise<FieldFormatter | null>; getValueSuggestions(field: IField, query: string): Promise<string[]>; @@ -99,10 +96,6 @@ export class AbstractSource implements ISource { return this._inspectorAdapters; } - createDefaultLayer(options?: Partial<LayerDescriptor>): ILayer { - throw new Error(`Source#createDefaultLayer not implemented`); - } - async getDisplayName(): Promise<string> { return ''; } @@ -155,10 +148,6 @@ export class AbstractSource implements ISource { return false; } - shouldBeIndexed(): boolean { - return false; - } - isESSource(): boolean { return false; } 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 509584cbc415a4..12e1780d9cad50 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 daae552a6f7726..792e7e862826c8 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 00000000000000..fbf5e25c78b170 --- /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 33f764784124e0..cb8f9c34e2b578 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 8b1ed588c8dd12..e970c75fa7adf0 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 4031a18bff7cb7..b1ba6fc6f6f8ea 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 77f8d88a8c0ab3..b1003d25fb7593 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,15 +5,13 @@ */ 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'; -import { ILayer } from '../../layer'; export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { defaultMessage: 'Tile Map Service', @@ -49,17 +47,6 @@ export class XYZTMSSource extends AbstractTMSSource { ]; } - createDefaultLayer(options?: LayerDescriptor): ILayer { - 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/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index a7e8be9fc4b460..d536b18af4aade 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -23,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; 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 2a4843c78635f8..bf1f9de6b2d2be 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 758cc35f41fbb7..8398bd7af39adc 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.js b/x-pack/plugins/maps/public/layers/vector_layer.js index 17b7f8152d76da..582e34bce2e985 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(); } diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b9..c3245e8e98db2e 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -36,12 +36,15 @@ function fetchFunction(...args) { return fetch(...args); } +export function isEmsEnabled() { + return getInjectedVarFunc()('isEmsEnabled', true); +} + let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { + if (isEmsEnabled()) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -86,7 +89,7 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!getInjectedVarFunc()('isEmsEnabled', true)) { + if (!isEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index 4d0f652af982a2..bc881d06f62ced 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -22,4 +22,6 @@ 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/plugins/ml/common/constants/settings.ts b/x-pack/plugins/ml/common/constants/settings.ts new file mode 100644 index 00000000000000..2df2ecd22e0788 --- /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/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 2a449c95faa5b5..da5fd3ac252096 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from 'kibana/server'; export const userMlCapabilities = { + canAccessML: false, // Anomaly Detection canGetJobs: false, canGetDatafeeds: false, @@ -18,6 +19,10 @@ export const userMlCapabilities = { canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, + // Annotations + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, }; export const adminMlCapabilities = { @@ -26,9 +31,11 @@ export const adminMlCapabilities = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canUpdateJob: false, canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, canStartStopDatafeed: false, - canUpdateJob: false, canUpdateDatafeed: false, canPreviewDatafeed: false, // Calendars @@ -38,8 +45,8 @@ export const adminMlCapabilities = { canCreateFilter: false, canDeleteFilter: false, // Data Frame Analytics - canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, }; @@ -47,7 +54,9 @@ export type UserMlCapabilities = typeof userMlCapabilities; export type AdminMlCapabilities = typeof adminMlCapabilities; export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; -export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array<keyof MlCapabilities>; +export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array< + keyof MlCapabilities +>; export function getDefaultCapabilities(): MlCapabilities { return { @@ -56,6 +65,23 @@ export function getDefaultCapabilities(): MlCapabilities { }; } +export function getPluginPrivileges() { + const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); + const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); + const allMlCapabilities = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; + + return { + user: { + ui: userMlCapabilitiesKeys, + api: userMlCapabilitiesKeys.map(k => `ml:${k}`), + }, + admin: { + ui: allMlCapabilities, + api: allMlCapabilities.map(k => `ml:${k}`), + }, + }; +} + export interface MlCapabilitiesResponse { capabilities: MlCapabilities; upgradeInProgress: boolean; diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts deleted file mode 100644 index f2ddadccb21703..00000000000000 --- 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/util/job_utils.d.ts b/x-pack/plugins/ml/common/util/job_utils.d.ts index bfad422e0ab48d..4528fbfbb774d7 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 de0aa4b8866297..8fe5733ce67bd6 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 f1facd18b9da5b..e9796fcbb0fe44 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/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 475e44af3669c0..c65d872212ad67 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/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 30fc74acbabf49..32b51c8b7d4eef 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/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 7b6464570e55c0..7d966949624c1f 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/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 306fd82dc87582..9dda8eec206e45 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 50a84eb3d11cb0..69df2773f9f8d7 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 a6fe9e1d119535..6bc5c9b15074fa 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/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index b2eda12abc578c..c379cd702daee0 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 5f5c3f7c28670a..dac39b1a2071da 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 219c195bab111e..3e4e9cfbd2b663 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/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 3fd228377c57e4..5a062320ca6c5c 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 934a0a5e9ae3af..356da38d5ad08b 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 4697496270edf2..8070f94a1264dd 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 b51be4d2486839..d37d1fd815eb57 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 7cef6f17bbefb4..00000000000000 --- 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 6e638d647a3875..175c20bf49c947 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/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 5093801d2d1846..b6e95ae8373ee1 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -36,7 +36,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(22); + expect(count).toBe(28); done(); }); }); @@ -49,28 +49,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -84,28 +98,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(true); expect(capabilities.canDeleteJob).toBe(true); expect(capabilities.canOpenJob).toBe(true); expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canCreateDatafeed).toBe(true); + expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(true); expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); expect(capabilities.canCreateDataFrameAnalytics).toBe(true); expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); @@ -119,28 +147,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -154,28 +196,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -189,28 +245,42 @@ describe('check_capabilities', () => { mlLicense, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -225,28 +295,43 @@ describe('check_capabilities', () => { mlLicenseBasic, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(false); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index a2ad83c5522de3..d955cf981faca6 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -44,4 +44,6 @@ function disableAdminPrivileges(capabilities: MlCapabilities) { Object.keys(adminMlCapabilities).forEach(k => { capabilities[k as keyof MlCapabilities] = false; }); + capabilities.canCreateAnnotation = false; + capabilities.canDeleteAnnotation = false; } diff --git a/x-pack/plugins/ml/server/lib/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts new file mode 100644 index 00000000000000..38b1f5e3fc0834 --- /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/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 84f81a30f36b81..40b2a524151b39 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 67e80a3bc44c05..969b74194148b3 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -45,8 +45,9 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; -import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { getPluginPrivileges } from '../common/types/capabilities'; import { setupCapabilitiesSwitcher } from './lib/capabilities'; +import { registerKibanaSettings } from './lib/register_settings'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -74,8 +75,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug } public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { - const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); - const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); + const { user, admin } = getPluginPrivileges(); plugins.features.registerFeature({ id: PLUGIN_ID, @@ -97,30 +97,33 @@ 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: userMlCapabilitiesKeys, + ui: user.ui, }, }, { id: 'ml_admin', privilege: { + api: admin.api, app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID], savedObject: { all: [], read: [], }, - ui: [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys], + ui: admin.ui, }, }, ], }, }); + registerKibanaSettings(coreSetup); this.mlLicense.setup(plugins.licensing.license$, [ (mlLicense: MlLicense) => initSampleDataSets(mlLicense, plugins), @@ -165,7 +168,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug indicesRoutes(routeInit); jobAuditMessagesRoutes(routeInit); jobRoutes(routeInit); - jobServiceRoutes(routeInit, { resolveMlCapabilities }); + jobServiceRoutes(routeInit); notificationRoutes(routeInit); resultsServiceRoutes(routeInit); jobValidationRoutes(routeInit, this.version); diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index d5abebda00caa0..e6d082cd74aabd 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 ca63d69f403f6b..63cd5498231af5 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/_stats', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -154,6 +166,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -188,6 +203,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: anomalyDetectionUpdateJobSchema, }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -220,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canOpenJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -251,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -286,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -319,6 +346,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.any(), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -351,6 +381,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: forecastAnomalyDetector, }, + options: { + tags: ['access:ml:canForecastJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +422,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getRecordsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -425,6 +461,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: getBucketParamsSchema, body: getBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -462,6 +501,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getOverallBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -496,6 +538,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: getCategoriesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index a17601f74ae93e..9c80651a13999e 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/calendars', validate: false, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdsSchema, }, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; @@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { params: calendarIdSchema, body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdSchema, }, + options: { + tags: ['access:ml:canDeleteCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index dd9e0ea66aa9dd..32cb2b343f8760 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics/_stats', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, body: dataAnalyticsJobConfigSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsEvaluateSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsExplainSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canDeleteDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, query: stopsDataFrameAnalyticsJobQuerySchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 20029fbd8d1a6b..04008a896a1a22 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -88,6 +88,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerFieldStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,6 +153,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerOverallStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index ec667e1d305f52..1fa1d408372da3 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds/_stats', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canCreateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canUpdateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, query: deleteDatafeedQuerySchema, }, + options: { + tags: ['access:ml:canDeleteDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: startDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 577e8e0161342a..b0f13df294145e 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -46,8 +46,10 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getCardinalityOfFieldsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, - mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -79,6 +81,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getTimeFieldRangeSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 3f3fc3f547b6a0..0f389f9505943b 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['text/*', 'application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { @@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 738c25070358d1..d5287c349a8fca 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: createFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { params: filterIdSchema, body: updateFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canDeleteFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/_stats', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e434936beba630..fb3ef7fc41c767 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: indicesSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1fe5a7af95d4f0..5acc89e7d13be7 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -33,6 +33,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -67,6 +70,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio validate: { query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 149ca2591fd765..05c44e1da97575 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization, JobServiceRouteDeps } from '../types'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -27,19 +25,7 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes( - { router, mlLicense }: RouteInitialization, - { resolveMlCapabilities }: JobServiceRouteDeps -) { - async function hasPermissionToCreateJobs(request: KibanaRequest) { - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('resolveMlCapabilities is not defined'); - } - - return mlCapabilities.canCreateJob; - } - +export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * @@ -55,6 +41,9 @@ export function jobServiceRoutes( validate: { body: forceStartDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -86,6 +75,9 @@ export function jobServiceRoutes( validate: { body: datafeedIdsSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +109,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -148,6 +143,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -184,6 +182,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -215,6 +216,9 @@ export function jobServiceRoutes( validate: { body: schema.object(jobsWithTimerangeSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -245,6 +249,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -272,6 +279,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/groups', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -302,6 +312,9 @@ export function jobServiceRoutes( validate: { body: schema.object(updateGroupsSchema), }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -329,6 +342,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -359,6 +375,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +408,9 @@ export function jobServiceRoutes( params: schema.object({ indexPattern: schema.string() }), query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -422,6 +444,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -474,6 +499,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -522,6 +550,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -552,6 +583,9 @@ export function jobServiceRoutes( validate: { body: schema.object(lookBackProgressSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -583,17 +617,12 @@ export function jobServiceRoutes( validate: { body: schema.object(categorizationFieldExamplesSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - // due to the use of the _analyze endpoint which is called by the kibana user, - // basic job creation privileges are required to use this endpoint - if ((await hasPermissionToCreateJobs(request)) === false) { - throw Boom.forbidden( - 'Insufficient privileges, the machine_learning_admin role is required.' - ); - } - const { validateCategoryExamples } = categorizationExamplesProvider( context.ml!.mlClient.callAsCurrentUser, context.ml!.mlClient.callAsInternalUser @@ -644,6 +673,9 @@ export function jobServiceRoutes( validate: { body: schema.object(topCategoriesSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index dd2bd9deadf43f..632166d6d5fb8f 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: estimateBucketSpanSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: modelMemoryLimitSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateCardinalitySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateJobSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2891144fc4574a..622ae66ede4264 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -97,6 +97,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { indexPatternTitle: schema.string(), }), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ...getModuleIdParamSchema(true), }), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -161,6 +167,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { params: schema.object(getModuleIdParamSchema()), body: setupModuleBodySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -218,6 +227,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { validate: { params: schema.object(getModuleIdParamSchema()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 59458b1e486db2..e4a9abb0784beb 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/notification_settings', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 89c267340fe52f..94ca0827ccfa59 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: anomaliesTableDataSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryDefinitionSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: maxAnomalyScoreSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryExamplesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: partitionFieldValuesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d5fe45728c56c7..7ae7dd8eef0653 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -54,6 +54,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -110,6 +113,9 @@ export function systemRoutes( { path: '/api/ml/ml_capabilities', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,7 +156,11 @@ export function systemRoutes( { path: '/api/ml/ml_node_count', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { // check for basic license first for consistency with other @@ -201,6 +211,9 @@ export function systemRoutes( { path: '/api/ml/info', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -229,6 +242,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index d4cd61a7fa4f71..678e81d3526ac8 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -30,10 +30,6 @@ export interface SystemRouteDeps { resolveMlCapabilities: ResolveMlCapabilities; } -export interface JobServiceRouteDeps { - resolveMlCapabilities: ResolveMlCapabilities; -} - export interface PluginsSetup { cloud: CloudSetup; features: FeaturesPluginSetup; diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index edd6142455dfb6..eeed7b4d5acf61 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/remote_clusters/public/application/_hacks.scss b/x-pack/plugins/remote_clusters/public/application/_hacks.scss new file mode 100644 index 00000000000000..b7d81885e716d0 --- /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 f2d788c7413428..cf6e855ba58dfa 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/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index 3f7c210eb3696b..e8916ff82e1217 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/rollup/README.md b/x-pack/plugins/rollup/README.md new file mode 100644 index 00000000000000..b43f4d59814092 --- /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 aeffa3dc3959fd..e94726a6f3d956 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 8f832f6c6a345a..4c7dcb48a4d3f0 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,6 +4,17 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"], - "requiredPlugins": ["management", "data"] + "requiredPlugins": [ + "indexPatternManagement", + "management", + "licensing", + "data" + ], + "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 4a55c4679c3d86..eca624e16cb868 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 aa1cc2dfea323d..5d9340a140500b 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 42c950f0b0d74a..9d81abf70a55d4 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/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 00000000000000..6d02600521c3a6 --- /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 40568424537765..78859a959a1e0b 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 00000000000000..c03b7c33abe0a1 --- /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 00000000000000..9dde027cd69492 --- /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/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/rollup/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/rollup/server/lib/is_es_error.ts 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 93c4c1b52140b2..333863979ba953 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 5a57129aa60395..151afe660847f3 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 9d5aad2c2d3bca..815fe163411b39 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 ea6d197e22029d..ee9a1844c7468b 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 ad621f2d9ba806..b06cf971a64609 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 00000000000000..7bf525ca4aa984 --- /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 00000000000000..32f23314c52590 --- /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 00000000000000..0aa5772b56991e --- /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 00000000000000..3521650c1dc3e5 --- /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 00000000000000..9e22060b9beb7f --- /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 00000000000000..fe1d1c6109a889 --- /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 00000000000000..adf8c1da0af0ed --- /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 00000000000000..32f7b3f35e1636 --- /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 00000000000000..a8d51f4639fc6c --- /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 00000000000000..fb6f2b12ba52ed --- /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 00000000000000..118d98e36e03cf --- /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 00000000000000..2a2d823e79bc66 --- /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 00000000000000..c5c56336def1a3 --- /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 00000000000000..b25480855b4a21 --- /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 00000000000000..7d7cce3aab3343 --- /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 00000000000000..7f79c4f4465467 --- /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 00000000000000..bfd357867c3e21 --- /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 00000000000000..09842f529abed3 --- /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 00000000000000..c21d76400164ea --- /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 5184ab0e962bd2..571d2630b2b177 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 5bdfa7d4886aac..9dec665614635f 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 34bc44b88e40d9..7815798d6a9f3d 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 4342e82d2f90b1..fd2b1cb8d1cf7e 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 00000000000000..c668c6ccf71d16 --- /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 cd3ef34858b19c..41567a04fe0302 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 0a423c00f0218a..00000000000000 --- 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 00000000000000..2227cbe8a495cf --- /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 00000000000000..add2db6a3c170d --- /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 00000000000000..156a76542a28fb --- /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 00000000000000..08e7be248619f0 --- /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 00000000000000..89b7489d45ebb2 --- /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 00000000000000..a34dcb18d2b9cc --- /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 00000000000000..8f7661a89a269d --- /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 2e73b8cd044826..6657f5c0a900cf 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'; @@ -46,6 +47,7 @@ export class AuthenticationService { ((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 }); 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 dfa258d523c5a4..00000000000000 --- 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 3590fa460a4010..585dc368da7077 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 f7cdd751437919..00000000000000 --- 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 e1292c5b21536e..946c58a1c8e998 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 aa306611299783..35be650d127fb7 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/login/__snapshots__/login_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap index ecbdfedac1dd38..bbc6bfa1faddc7 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 4dd2c0cabfb5e8..00000000000000 --- 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 7b8283b7bec0e6..072a025aa06a07 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 00000000000000..6784052ef4337e --- /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 c17c10a2c51484..4e172cdde0eeda 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 01f5c40a69aebd..460c6550085a45 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 c4be57d8d7db7d..ab107e46dfff61 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 70f8f76ee0a9c0..d24a301ed24ec7 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 2ff760891fa4e2..02b1a7d0d3fa03 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/index.scss b/x-pack/plugins/security/public/index.scss deleted file mode 100644 index 999639ba22eb78..00000000000000 --- 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 458f7ab801fdf9..8016c942240601 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 5d419b53230799..00000000000000 --- 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_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 272fc9cfc2fe6a..b9ec5b35b3f9df 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 bae6effcd2ec56..00000000000000 --- 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 3f240b8a2b2a23..00000000000000 --- 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 c3b2764e647130..00000000000000 --- 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 c17a853a654676..b10a6dd8d183fc 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 ea090520fdd466..ffb6d6d98f180a 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 5256c79f01f10f..00000000000000 --- 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 0153b1734ceba0..00000000000000 --- 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 c0f4f8ab9a8701..00000000000000 --- 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 01af7cb4509f64..eb1417600e19b5 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 a1a9d038065e61..00000000000000 --- 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/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss deleted file mode 100644 index 19547c0e1953e0..00000000000000 --- 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 6a96553742819f..00000000000000 --- 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 2083778e539980..5d7b13acf79da6 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 9e4a3a8a99b565..77445952f3d692 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 3f40f21e102a1d..00000000000000 --- 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 8f47727fdf8d62..00000000000000 --- 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 00000000000000..8e2a3b0512afbf --- /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 ccb5398a11b236..30a275876fdc79 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 b40a32cb8df96b..00000000000000 --- 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 92e42ec811afce..63ee311f3155ed 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 e1a10fdc2b8c33..9aaa3b47f3b193 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 35df0c1b965837..00000000000000 --- 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 7b24b74aceba0c..00000000000000 --- 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 734ba7882ba72c..00000000000000 --- 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 00000000000000..727fac4782752d --- /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 6417ce81b647d5..1c8130029bb501 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 82a2b8d2a98ada..9d337c1508ad48 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/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index eca3e7d6727df8..11aadcff377ef7 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 bd6dbad7dbf149..b06d8fffd4b629 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 e9c4b6e281cf3e..00000000000000 --- 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 f7ee210a21a741..4dfd69a2ccb1ff 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 40b525b5d21888..d7243ecbe13f87 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 c14b98ed4781eb..888aa3361faf04 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/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index a595b63faaf9b3..49b7b40659cfc1 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 caf5b485d05e35..58dea2b23e5463 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/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 9397a7a42b3262..7cd3ac18634f78 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -19,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 6609f8707976b7..1c1e0ed781f18e 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 d76a5a533d4983..779b852195b028 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,6 +177,7 @@ 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) => diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 46a7ee79ee60c5..2c248646499774 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 97ff7d00a43362..8fe79a788ac51c 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 ababf12c2be606..a6407366bbd3b2 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 3ce0198273af97..d58c999ddccdf8 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -50,21 +50,6 @@ 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], }, @@ -72,14 +57,10 @@ describe('Security Plugin', () => { "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 { @@ -107,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 9dd4aaafa3494f..97f5aea888dc7c 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/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 156c03e90210b7..5a0401e6320b46 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 abab67c9cd1d28..91783140539a5b 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 b113b2ca59e3ef..d8c212aa2d2174 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 fd05821f9d5206..c163ff4e256cd2 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 00000000000000..3d616575b84131 --- /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 00000000000000..49e1ff42a28a2a --- /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 a8e7e905b119af..7cddef9bf2b982 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 255989dfeb90cd..b9de58d47fe407 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 3ff05d242d9dde..7cb73c49f9cbc8 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 d43319efbdfb9f..014ad390a3d53b 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 4d6747de713f76..f72facb2e24cc9 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 96ab5dadbda10d..8dc2df600b2114 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 edde5c6b8fa0d0..bcf5c78be36445 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/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts new file mode 100644 index 00000000000000..55b4f9c6aca4dc --- /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 00000000000000..c8e674997c19c1 --- /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 00000000000000..89a619598f7c19 --- /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 b46ccdbbe3d05a..a12dd926a91810 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as runtimeTypes from 'io-ts'; import { ReactNode } from 'react'; // This type is for typing EuiDescriptionList @@ -11,3 +12,6 @@ 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/kibana.json b/x-pack/plugins/siem/kibana.json index 1eb1a7dbde8761..e0ff82eb10eb22 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -3,8 +3,23 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "siem"], - "requiredPlugins": ["actions", "alerting", "features", "licensing"], - "optionalPlugins": ["encryptedSavedObjects", "ml", "security", "spaces"], + "requiredPlugins": [ + "actions", + "alerting", + "data", + "embeddable", + "esUiShared", + "features", + "home", + "inspector", + "kibanaUtils", + "licensing", + "maps", + "triggers_actions_ui", + "uiActions", + "usageCollection" + ], + "optionalPlugins": ["encryptedSavedObjects", "ml", "newsfeed", "security", "spaces"], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json index 31c930dce71c06..829332918d3c48 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/legacy/plugins/siem/public/app/app.tsx b/x-pack/plugins/siem/public/app/app.tsx similarity index 88% rename from x-pack/legacy/plugins/siem/public/app/app.tsx rename to x-pack/plugins/siem/public/app/app.tsx index 44c1c923cd6eea..6e2a4642f99a46 100644 --- a/x-pack/legacy/plugins/siem/public/app/app.tsx +++ b/x-pack/plugins/siem/public/app/app.tsx @@ -18,13 +18,13 @@ 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 { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { DEFAULT_DARK_MODE } from '../../../../../plugins/siem/common/constants'; +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 { CoreStart, StartPlugins } from '../plugin'; +import { StartServices } from '../plugin'; import { PageRouter } from '../routes'; import { createStore, createInitialState } from '../store'; import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; @@ -95,21 +95,18 @@ const StartAppComponent: FC<AppFrontendLibs> = libs => { const StartApp = memo(StartAppComponent); interface SiemAppComponentProps { - core: CoreStart; - plugins: StartPlugins; + services: StartServices; } -const SiemAppComponent: React.FC<SiemAppComponentProps> = ({ core, plugins }) => ( +const SiemAppComponent: React.FC<SiemAppComponentProps> = ({ services }) => ( <KibanaContextProvider services={{ appName: 'siem', storage: new Storage(localStorage), - ...core, - ...plugins, - savedObjects: core.savedObjects, + ...services, }} > - <StartApp {...compose(core)} /> + <StartApp {...compose(services)} /> </KibanaContextProvider> ); 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 00000000000000..7275a718564eff --- /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 dd608babef48fc..d545a071c3ea68 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 00000000000000..957feb6244792a --- /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 00000000000000..321f7214c8fef9 --- /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 00000000000000..28355372df1463 --- /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/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx rename to x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx index 8f261da629f949..dccc156ff6e447 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx +++ b/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { QuerySuggestion, QuerySuggestionTypes, -} from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; import { SuggestionItem } from '../suggestion_item'; const suggestion: QuerySuggestion = { 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 00000000000000..72236d799f995c --- /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 00000000000000..9821bb6048b513 --- /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 f99a545d558f76..be9a9817265b0a 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 c7b40c50ffde89..74d728e65f0189 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 f8b5eb7209ff4b..1d9508fc28f3d9 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/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/plugins/siem/public/components/draggables/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/index.tsx rename to x-pack/plugins/siem/public/components/draggables/index.tsx 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 00000000000000..d2dd3e54293414 --- /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 f4e6ee5f878a63..aaae43d9684af0 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 0c7a1212ba280f..dd7e1cd6ea9bac 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 8c96e0b75a1365..0d1cd515820c58 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 95% 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 ef2cd856674089..7c2d5e51d813f6 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 00000000000000..d8e20c7f47b4ed --- /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 d210c749dae9c7..aff66396af39d3 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 00000000000000..bc6a1b3b77bfa9 --- /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 00000000000000..404ca4a16e0f1c --- /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 00000000000000..98a1acf471629e --- /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 00000000000000..0f6c5c2e139a7e --- /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 00000000000000..e1559cf9e0c48d --- /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 94% 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 47dd0dc55d7032..43b50c24f6b5b7 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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/legacy/plugins/siem/public/components/link_icon/index.tsx b/x-pack/plugins/siem/public/components/link_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx rename to x-pack/plugins/siem/public/components/link_icon/index.tsx 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 00000000000000..6d473f47217105 --- /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/legacy/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/plugins/siem/public/components/markdown_editor/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx rename to x-pack/plugins/siem/public/components/markdown_editor/index.tsx 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 00000000000000..c4bdff7ea649a7 --- /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 00000000000000..c59775ad325d07 --- /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 cebfc172ee6fff..d64bd3a64e941f 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 b5aacdf664c67a..b7c544273ae927 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 e27e9dc084c573..54773e3ab6dda7 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 24f203a3682d56..e7615bf3b89ba8 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 bc488ee00988b2..7bcbf4afa10cc4 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 a0343608dc67a8..e7b14f2e80bf24 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 bd7d696757ca6d..3c93e1c195cd7a 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 00000000000000..58d40c298b3294 --- /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 00000000000000..85d77485830a52 --- /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 00000000000000..291cb90098f78e --- /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 00000000000000..d8b62029138c82 --- /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 00000000000000..99ded06cfdcc86 --- /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 00000000000000..2e2dea09f8c38b --- /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 00000000000000..e8a2865938062b --- /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 00000000000000..96bd9b08bf8bf0 --- /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 00000000000000..ae41737c5bcad3 --- /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 100% 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 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 00000000000000..12cf952bb1ff86 --- /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 00000000000000..ea28bc06ef915b --- /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 00000000000000..d26d02780ffba8 --- /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 6b2f953b82de42..26aeab87e3510c 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 00000000000000..46a0d46c1e0d16 --- /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 00000000000000..41999c62492776 --- /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 00000000000000..6573b6371e9a49 --- /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 00000000000000..7aed36422bd2f5 --- /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 00000000000000..4d0e6a737d303f --- /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 00000000000000..5feb2ef73c57f3 --- /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 00000000000000..56b59ca97156f6 --- /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 00000000000000..52c142ceff4805 --- /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 00000000000000..d649a0dd9e9237 --- /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 cada0a9aff9390..7615001eec9da3 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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap 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 00000000000000..94dac6607ce217 --- /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 00000000000000..a815ecd1005180 --- /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 00000000000000..e27669b2b15be2 --- /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 00000000000000..1ad7bc16b901ee --- /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 00000000000000..b641038f35ba6d --- /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 793737a1ad754c..4e700a46ca0e2d 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 00000000000000..b6b515ceeffa61 --- /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 00000000000000..ad38a7d61bcba6 --- /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 97% 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 d05ab6bcc378f9..66c559729cccdf 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 98% 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 31525a4904bc23..4cb8140e22ceff 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 95% 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 ddb07b4636b886..60c868f780ff3d 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 97% 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 a5525df5ef3c36..e04aed17c6d671 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 97% 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 0ae952412a973d..3a691d2bbc6219 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 any; // eslint-disable-line @typescript-eslint/no-explicit-any +` 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 a4de8ffa3b9c5a..0c8a6932adf91e 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 00000000000000..6f2053488f69bb --- /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 00000000000000..fc5a8ae924f828 --- /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 00000000000000..53ab7d81cadc26 --- /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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx 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 00000000000000..4c64c8a100b41a --- /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 00000000000000..a78e5b8e1d2264 --- /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 00000000000000..7d2b4f71183dde --- /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 00000000000000..fa92ef9ce59652 --- /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 02a575db259bbe..0b8ed71135744f 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 00000000000000..4cc89e5bdba73d --- /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 222cc0530bddbd..10f10b1a86f1ec 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 7c1eadd8e8beda..25a0078b6066a6 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 00000000000000..9325dcf499b2b4 --- /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 00000000000000..9863df42f101d3 --- /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 136252617e2a26..d8dc63ef92ec60 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 00000000000000..62196b90e5e6f2 --- /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 4838fb4499b87e..54a196d1b81617 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 dc1b8d428bb20d..974bee53bc2ba0 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 00000000000000..2bbb4cde92b151 --- /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 00000000000000..f6cae81e3c6c4b --- /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 00000000000000..790a797b2fead4 --- /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 00000000000000..6d4a88c45a7681 --- /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 00000000000000..dcc31401564b1f --- /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 99% 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 693a7175ebc3eb..ad61e2b46f6c5f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/api.test.tsx @@ -6,7 +6,7 @@ import { KibanaServices } from '../../lib/kibana'; -import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { CASES_URL } from '../../../../case/common/constants'; import { deleteCases, 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 00000000000000..b97f94a5a6b597 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/api.ts @@ -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 { + 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: 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 00000000000000..c3611f490708ae --- /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 00000000000000..4f516764e46f3d --- /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 00000000000000..c6824bd50edb5c --- /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 00000000000000..ed95315c066dcb --- /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 00000000000000..2826e9a2c2e55c --- /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 00000000000000..a185d435f71651 --- /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/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/plugins/siem/public/containers/case/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/constants.ts rename to x-pack/plugins/siem/public/containers/case/constants.ts 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 00000000000000..0f44b3a1594ba0 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/mock.ts @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 = { + 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/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 00000000000000..dde13dc38aca84 --- /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 2fc9b8294c8e0e..01679ae4ccd82d 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 aeb50fc098eeed..b33269f26e97dd 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 c6d34b54499777..c7d3b4125aada8 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 100% 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 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 99% 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 89e7e18cf06881..acd4b92ee430dc 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'; 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 7ebbbba076c121..2f2fe18321246f 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 00000000000000..aaa5ff4ab44c13 --- /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 00000000000000..c1fadf289ef4d9 --- /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 83b8a3581a4be1..8c688fe5615f00 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 c5aefac15f48e1..7e222045a1a3ba 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 00000000000000..f89d21ef1aeb19 --- /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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx 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 00000000000000..1397e4a8696be8 --- /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 00000000000000..9cae503d309408 --- /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 00000000000000..43f55dfcf27776 --- /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 00000000000000..5d378d79acc7a4 --- /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 00000000000000..5f66e3f4b88d4d --- /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 00000000000000..a460fa8999b57b --- /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 00000000000000..f59df84dacc1b1 --- /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 00000000000000..733c2224d840a7 --- /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 00000000000000..5057e872b53131 --- /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 00000000000000..ade94c430c6efb --- /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 00000000000000..de9d54b1a185c8 --- /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 00000000000000..5be2423e8a162a --- /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 00000000000000..338cdc39b178c1 --- /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 00000000000000..6120538a01e78b --- /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 00000000000000..18bb611191bbc8 --- /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 00000000000000..04c8783c30a0ff --- /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 00000000000000..bf4e64f63d5599 --- /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 00000000000000..bd1e1a002bbcdc --- /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 00000000000000..f0f1f8257f29f1 --- /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 00000000000000..2dd9ccf24d802f --- /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 00000000000000..d0acd41c224a5f --- /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 c33f5fd89a79b4..dfb452c24b86e5 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 45041a64476118..db618f216d83e7 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 00000000000000..e9359fdb195877 --- /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 00000000000000..092aad9e7400cc --- /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 e380e46e77070c..7d30b6c22a1100 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 00000000000000..62c8d21a2e9448 --- /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 00000000000000..023e2e6af9f88e --- /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 00000000000000..cf1b8954307e7b --- /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 00000000000000..6e09e124696b6f --- /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 00000000000000..3738355c8846eb --- /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 00000000000000..0a2ce67d9be805 --- /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 00000000000000..5f71449c524606 --- /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 2a9dd8f2aacfe8..4026a043c7778c 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 00000000000000..8c39d5e58b99e1 --- /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 00000000000000..6527904964d000 --- /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 00000000000000..46f72c1fa17c8d --- /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 c742ced4c504c7..fb30c9a5411ed7 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/legacy/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/connectors/config.ts rename to x-pack/plugins/siem/public/lib/connectors/config.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/lib/connectors/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/connectors/index.ts rename to x-pack/plugins/siem/public/lib/connectors/index.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg b/x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg 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/logos/servicenow.svg diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx rename to x-pack/plugins/siem/public/lib/connectors/servicenow.tsx index 536798ffad41b5..9fe0b4a957cebd 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx @@ -21,7 +21,7 @@ import { ValidationResult, ActionParamsProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; +} from '../../../../triggers_actions_ui/public/types'; import { FieldMapping } from '../../pages/case/components/configure_cases/field_mapping'; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts rename to x-pack/plugins/siem/public/lib/connectors/translations.ts 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 00000000000000..2def4b5107aee8 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/types.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 no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ConfigType, + SecretsType, +} from '../../../../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/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 00000000000000..810baa89cd60dc --- /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 00000000000000..c3e1f35f373569 --- /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 e1d0a445bf2fb0..d62701fe5944ab 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 00000000000000..88be8d25e5840a --- /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 00000000000000..4ab3e102f56ab5 --- /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/legacy/plugins/siem/public/lib/telemetry/index.ts b/x-pack/plugins/siem/public/lib/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/telemetry/index.ts rename to x-pack/plugins/siem/public/lib/telemetry/index.ts 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 59c6cb35669070..ca889e20e695f3 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 b72c34d3b59a71..1696001203bc87 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 266c3aadea8af1..6678c3043a3da4 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 00000000000000..b175ddbf5106db --- /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 00000000000000..cebba3e237f986 --- /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 952f7f51b63f29..59e3874c6d0a1b 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 363281e5633174..edd1c737718296 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 84% 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 cc01edcfaab112..12946c3af06bdb 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,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 { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +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' + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); export const mockFormHook = { isSubmitted: false, 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 00000000000000..7ba8ec96662532 --- /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 00000000000000..aa987b277da06a --- /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 00000000000000..ad73fd71b8e115 --- /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/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx diff --git a/x-pack/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx 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/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx 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 00000000000000..0eccd8980ccd23 --- /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 100% 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 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 00000000000000..545eceb0f73a1b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -0,0 +1,782 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or 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, + 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 00000000000000..a970fe895eb716 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { 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 { 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[] = [ + { + 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, + 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 00000000000000..4c2e15ddfa98a9 --- /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 00000000000000..6731b88572cdd3 --- /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 00000000000000..a4e0bb69165317 --- /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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx 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 00000000000000..9ddb96a4ed295a --- /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/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx 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 00000000000000..d428d9988ae39f --- /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 00000000000000..6109fd05096b9c --- /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 00000000000000..96f87c90829452 --- /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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx 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 00000000000000..f1aea747485e41 --- /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 6212cad7e18459..5428b9932fbde7 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 fd3b9a6f68e824..81b643b7894dfc 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 00000000000000..a948d2b940b0c9 --- /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 00000000000000..3fa2da37046b00 --- /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 00000000000000..5442c8c19b5a70 --- /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 00000000000000..b9268716f85f01 --- /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 00000000000000..24b12cae62d85d --- /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 00000000000000..cf6730ea3ec3d7 --- /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 00000000000000..66964fae70f941 --- /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/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 97c89f91c12bdf..d383b5cd464ce2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -19,7 +19,7 @@ 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 { 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'; 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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx 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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx diff --git a/x-pack/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx 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 00000000000000..186aeae42246d3 --- /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 00000000000000..1ac371a3f68297 --- /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 00000000000000..fdfcfd0fd85fe3 --- /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 00000000000000..108f2138114122 --- /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 5e8681a90d428b..79993c37e549c3 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 00000000000000..564a3c5dc2c018 --- /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 00000000000000..4fb9faaea711c0 --- /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 00000000000000..b92d98a4afb13b --- /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 00000000000000..b743888b678155 --- /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 00000000000000..6f3d299da8d452 --- /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 00000000000000..b6887badc56be5 --- /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 00000000000000..8915c5f0a224f9 --- /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 00000000000000..0cf15c41a0f913 --- /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 00000000000000..7ad116c313361d --- /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 00000000000000..f2a04a87ced278 --- /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 00000000000000..2ccbffd864070e --- /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 00000000000000..dcb5397d28f7ce --- /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 00000000000000..f93ad94dd462b0 --- /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 00000000000000..a9e0962f16e6ed --- /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 00000000000000..d989d709cc4aff --- /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 00000000000000..6da76f2fb5cac9 --- /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 00000000000000..ef04288aa1b6fe --- /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 00000000000000..af4ba8eb091e20 --- /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 99cf767c65e08b..6134c1dd6911a2 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 d574925a916009..028c804f4d48fe 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 00000000000000..ec33834b1bf734 --- /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 00000000000000..20d4d4e463a7f0 --- /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 00000000000000..350d6e34c1c0f7 --- /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 00000000000000..11c41fc74515e2 --- /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 00000000000000..9d15d7ee250c99 --- /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 00000000000000..4c4f6c06ce1e1a --- /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 00000000000000..ee03bff99b9677 --- /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 797fef15865184..300cb83c4ce753 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 9b1ee76e1d376e..65b3142c8c0745 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 00000000000000..a1936cf9221f85 --- /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 00000000000000..f242b0d84d7c13 --- /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 00000000000000..77d6da7a7efc4d --- /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 b20cd842955666..c129258fa2e87b 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 4d4d96803cd658..c972fd83cc88f8 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 00000000000000..6357d93d7ba71b --- /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/legacy/plugins/siem/public/pages/timelines/translations.ts b/x-pack/plugins/siem/public/pages/timelines/translations.ts similarity index 78% rename from x-pack/legacy/plugins/siem/public/pages/timelines/translations.ts rename to x-pack/plugins/siem/public/pages/timelines/translations.ts index 723d164ad2cdd5..304474bbff2c54 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/translations.ts +++ b/x-pack/plugins/siem/public/pages/timelines/translations.ts @@ -23,3 +23,10 @@ export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( 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 00000000000000..e54c96364ba6b0 --- /dev/null +++ b/x-pack/plugins/siem/public/plugin.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 { 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 } 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> { + private kibanaVersion: string; + + constructor(initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup, 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()); + + 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 00000000000000..472006a9e55b15 --- /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 00000000000000..04cdf5246de2ce --- /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 00000000000000..3e6be6ce859e59 --- /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 00000000000000..686dc096e61b01 --- /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 d3559e7a7adde9..2af0f87b4494d9 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 00000000000000..12155decf40d44 --- /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 13d30825a169cb..bb6babcba41aeb 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 e6acff87364927..6812d8d8aa672b 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 4d1b73aa70a6ee..6a1dadb8a59f59 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 e5a712fe2c666f..3722a6ad8036ca 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 2260999a91e7bc..a1281250ba72af 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 00000000000000..19de49918d100a --- /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 00000000000000..15bd2980e4aebd --- /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 00000000000000..2c679ba41116ee --- /dev/null +++ b/x-pack/plugins/siem/public/store/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} 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 bb66067512d1fd..9dc179ba7a6e2d 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 89f7d34d851310..c4869a4851ae50 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 00000000000000..335398177f0f43 --- /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 0b5e5d6cf13b5b..9b4a57f09066d1 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 145d9715970c87..c8642e8b42c8fc 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'; 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 bded8832aba5a9..e6b063dfd2c071 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/tsconfig.json b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json index 42d26c4c27ed66..fb89c0e0fc3e24 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/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 9dd04247b7f47c..bc2b3a53d85f3e 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 d272b7ff59b79a..6a35ba08f8e43f 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/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index 9c46f3320e37ec..4a595032e43eb2 100644 --- a/x-pack/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/plugins/siem/server/lib/compose/kibana.ts @@ -27,9 +27,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 +41,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/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index d50c339c95266c..e50f82bb482a70 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 @@ -31,9 +31,8 @@ export const ruleActionsSavedObjectMappings = { type: 'keyword', }, params: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dynamic: true as any, - properties: {}, + type: 'object', + enabled: false, }, }, }, 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 750c5574f4a729..2028216e6770f2 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 8d8cbdd70a8036..c587e9a2041822 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 a56d788d69c16c..22b602f935187e 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/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d298f1cc7cbc66..a8cc6dc6804102 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow, set, omit } from 'lodash/fp'; +import { flow, omit } from 'lodash/fp'; +import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; @@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { } const omitDottedFields = omit(errantFields.map(field => field.name)); - const setNestedFields = errantFields.map(field => set(field.name, field.value)); - const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) => + set(_anomaly, field.name, field.value) + ); + const setTimestamp = (_anomaly: Anomaly) => + set(_anomaly, '@timestamp', new Date(timestamp).toISOString()); return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 3eae30625e4223..219465f5514574 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 { 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/types.ts b/x-pack/plugins/siem/server/lib/note/types.ts deleted file mode 100644 index f7a10317bd84d0..00000000000000 --- 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 1e3a481e17106f..c653f23d5c1499 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,174 +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 { 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/types.ts b/x-pack/plugins/siem/server/lib/pinned_event/types.ts deleted file mode 100644 index e598f039350470..00000000000000 --- 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 ea5db565483c83..bde24a338ec846 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 abe8de9bf5b94b..6b4017b5e4d5c7 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 19adb7ac1045a0..eeded1cc2532de 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 00000000000000..2c5547e39fc4e5 --- /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 686f2b491cf881..a832c818d48b07 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 a83c4437733021..304ca309775ff8 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 00000000000000..70ee1532395a5d --- /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 00000000000000..c456ae31fb7da6 --- /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/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 9f41943cfa27f7..56c152d02ae98d 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,11 +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",Invalid value "undefined" supplied to "file"' + [ + '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 9d148abf82cddd..bff89bdf9b5b21 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 '../../../config'; - -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 00000000000000..241d266a14c78f --- /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 056fdaf0d2515f..3b340b1c15359f 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 71627363ef0f88..1fd3a3554dc155 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 00000000000000..43f4208947aa50 --- /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 00000000000000..9c47488d47159a --- /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 00000000000000..a0f3d11a1533de --- /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 00000000000000..1036a74b74a03c --- /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 00000000000000..2c67a514cdf979 --- /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 677891fa16c024..ea9a5fab66805c 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,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -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, @@ -22,11 +15,20 @@ 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, @@ -126,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; }; @@ -139,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))), @@ -178,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 f69a715f9b2c99..9e120cdc023dc1 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 00000000000000..6a25d8def91160 --- /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 e8cd27947589fc..d2df7589f3c4a9 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 b956e0f98fcb62..1cab24d0879ff6 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 @@ -231,6 +231,15 @@ export const timelineSavedObjectMappings = { title: { type: 'text', }, + templateTimelineId: { + type: 'text', + }, + templateTimelineVersion: { + type: 'integer', + }, + timelineType: { + type: 'keyword', + }, dateRange: { properties: { start: { 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 0bce3300591c2d..00000000000000 --- 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/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3988fbec05de4f..dc484b75ab531a 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -38,6 +38,7 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; +import { APP_ID, APP_ICON } from '../common/constants'; export { CoreSetup, CoreStart }; @@ -62,7 +63,6 @@ export interface PluginSetup {} export interface PluginStart {} export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { - readonly name = 'siem'; private readonly logger: Logger; private readonly config$: Observable<ConfigType>; private context: PluginInitializerContext; @@ -70,7 +70,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S 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(); @@ -91,7 +91,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S 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), })); @@ -110,12 +110,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S ); 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'], diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 1c03823e85fd7a..ffad86a09cee72 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -30,6 +30,8 @@ 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 '../config'; @@ -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/utils/typed_resolvers.ts b/x-pack/plugins/siem/server/utils/typed_resolvers.ts index da38e8a1e1bf28..4f19bd54b01f0c 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/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index a70fbdb18c30b6..0f6e3fc31d96d6 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: unknown): 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/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts new file mode 100644 index 00000000000000..0ad9021cd7f39c --- /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/legacy/plugins/task_manager/server/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts similarity index 100% rename from x-pack/legacy/plugins/task_manager/server/migrations.ts rename to x-pack/plugins/task_manager/server/saved_objects/migrations.ts diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index 24ceea0fe71ef2..2a45a599120ddc 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -240,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!`); @@ -360,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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b55c7a15f1b3a..8974f0b5b4d582 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": "おっと!", @@ -2590,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": "緯度", @@ -2612,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": "作成", @@ -6381,7 +6350,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": "エンドポイント", @@ -8337,13 +8305,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": "概要", @@ -8422,8 +8383,6 @@ "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": "キャンセル", @@ -8446,9 +8405,7 @@ "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", @@ -8830,10 +8787,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": "マップ", @@ -10550,10 +10505,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": "実行中", @@ -12366,7 +12320,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": "戻る", @@ -12666,7 +12619,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": "システム管理者にお問い合わせください。", @@ -14149,7 +14101,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": "「コード」", @@ -14394,7 +14345,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": "以下を経由してユーザーを受け入れました:", @@ -15944,7 +15894,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}", @@ -16162,7 +16111,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": "ダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1758588d01ba80..d36a62f15aee9e 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": "喔哦!", @@ -2591,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": "纬度", @@ -2613,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": "创建", @@ -6383,7 +6352,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": "终端", @@ -8340,13 +8308,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": "概览", @@ -8425,8 +8386,6 @@ "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": "取消", @@ -8449,9 +8408,7 @@ "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", @@ -8833,10 +8790,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", @@ -10553,10 +10508,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": "正在运行", @@ -12370,7 +12324,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": "上一步", @@ -12670,7 +12623,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": "请联系您的管理员。", @@ -14153,7 +14105,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`", @@ -14398,7 +14349,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": "已接受该用户 - 通过", @@ -15949,7 +15899,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}", @@ -16167,7 +16116,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": "关闭", 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 9ce50cf47560ac..0a2ec3f203a9ac 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 d18cb21b3a0fe0..8078ef4938e50a 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 566ed7935e0137..9c017aa6fd31fd 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 263502a82ec795..971d6dbbb57bf0 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/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 d4def86b07b1f7..aed7d18bd9f3d5 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 4199cfb7b4b7fb..0027837c913d16 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 { @@ -111,14 +111,12 @@ export const ActionForm = ({ 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 +124,50 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // load connectors useEffect(() => { - loadConnectors(); + (async () => { + try { + setIsLoadingConnectors(true); + setConnectors(await loadConnectors({ http })); + } 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); - } - } 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 +186,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 +203,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 +226,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 +248,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 +275,9 @@ export const ActionForm = ({ ); return ( - <Fragment> + <Fragment key={index}> <EuiAccordion initialIsOpen={true} - key={index} id={index.toString()} className="actAccordionActionForm" buttonContentClassName="actAccordionActionForm__button" @@ -273,12 +289,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 +320,7 @@ export const ActionForm = ({ )} </EuiFlexItem> </EuiFlexGroup> - </p> + </div> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -349,10 +365,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 +379,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 +501,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 9da4f059f8967e..230b896eeca7da 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 5bfcf9fd2d4e6b..318dd28d92da1e 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 498ecffe9b947d..b9d08abae16842 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,13 @@ 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]); + }, [alert, loadAlertState, toastNotifications]); return alertState ? ( <AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} /> diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index bdca506cc73385..0cdf1ca05feac2 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/saved_object_types/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts new file mode 100644 index 00000000000000..dee0a74d8994bb --- /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 00000000000000..ba661fbeceb267 --- /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 00000000000000..b1321e634c0f1c --- /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/legacy/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/index.ts rename to x-pack/plugins/uptime/common/constants/index.ts 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 00000000000000..6064524872a0ac --- /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 100% rename from x-pack/legacy/plugins/uptime/common/constants/query.ts rename to x-pack/plugins/uptime/common/constants/query.ts 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/legacy/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts rename to x-pack/plugins/uptime/common/constants/settings_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/ui.ts rename to x-pack/plugins/uptime/common/constants/ui.ts 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/legacy/plugins/uptime/common/runtime_types/certs.ts b/x-pack/plugins/uptime/common/runtime_types/certs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts rename to x-pack/plugins/uptime/common/runtime_types/certs.ts 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/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts rename to x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts 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 00000000000000..e80471bf8b56f9 --- /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 100% 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 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 00000000000000..71ccd54dd3cdcd --- /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 6ec8a755ebea09..ce8b64ce07254a 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 00000000000000..65b80d08d4f20f --- /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 00000000000000..719dac022dada8 --- /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/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/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/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 a31a143b71fd24..ceb1e700f293e2 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/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 00000000000000..06cdb07bd8bcdb --- /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 c0b02181dcce1a..31cdcfac9feef3 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 c3e8579ca48370..6eec30d405f769 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 4963a901f0ecc5..7f19885c15406e 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 7e39b977f12710..52d4f620f84b3f 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_ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap 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/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_ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx 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 100% 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 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/legacy/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 similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx 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 04dfe4b3e3509a..92fc015a276d3f 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 09e6dc72b7f981..262e1552e3660d 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 13705e7d192933..9b1d3a73dc6614 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 100% 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 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 651103a34bf212..d38f203739ceac 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 1135b969018a11..aa4040e319e0f2 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/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx index 63aceed2be6362..792fff4c7cdca2 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -16,7 +16,7 @@ import { IIndexPattern, QuerySuggestion, DataPublicPluginSetup, -} from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; 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 97% 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 2b5ad9b59e39f8..936eae04ffa641 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,7 +14,7 @@ import { units, fontSizes, unit, -} from '../../../../../../apm/public/style/variables'; +} from '../../../../../../../legacy/plugins/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 95% 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 7fbabe71bcdb52..ac6832050b9d3c 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,7 +9,7 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { isEmpty } from 'lodash'; import Suggestion from './suggestion'; -import { units, px, unit } from '../../../../../../apm/public/style/variables'; +import { units, px, unit } from '../../../../../../../legacy/plugins/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 100% 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 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__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__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/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.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 100% 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 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__/monitor_page_link.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__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/overview/monitor_list/__tests__/monitor_page_link.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/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 96% 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 18e2e2437e1477..7e9536689470e6 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 @@ -202,13 +202,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 5bfe6ff0c5b4f1..6fb880e28c7347 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 100% 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 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/monitor_page_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.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/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx rename to x-pack/plugins/uptime/public/components/settings/certificate_form.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/plugins/uptime/public/components/settings/indices_form.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx rename to x-pack/plugins/uptime/public/components/settings/indices_form.tsx 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 00000000000000..243a25c26901a1 --- /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 137846de103b4d..4fabf3f2ed4972 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 00000000000000..e516ff44aa12ab --- /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 1ce00fe7ce3af5..306919015fcb1d 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/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/index.ts rename to x-pack/plugins/uptime/public/hooks/index.ts 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 ab4d6f75849e88..492d2bab5bb806 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 d1cc8e18973868..182c6b01141284 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/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 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts rename to x-pack/plugins/uptime/public/hooks/use_telemetry.ts 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 00000000000000..48cf2c90ad07b1 --- /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 71c73bf5ba5d4f..f7f9e056f41af5 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 00000000000000..f7ab254ffe675f --- /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 0624d20b197c0e..e7695fb1cbb56c 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 00000000000000..a5c2168378089d --- /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 b1a4d0a2aba0d8..0e5363527b5163 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/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/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/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 87% rename from x-pack/legacy/plugins/uptime/public/pages/monitor.tsx rename to x-pack/plugins/uptime/public/pages/monitor.tsx index 4495be9b24dc1f..8a309db75acd23 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -7,14 +7,13 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { useSelector } from 'react-redux'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; 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'; export const MonitorPage: React.FC = () => { const monitorId = useMonitorId(); 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 adc36efa6f7db1..fefd804cbfabf0 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 100% rename from x-pack/legacy/plugins/uptime/public/pages/page_header.tsx rename to x-pack/plugins/uptime/public/pages/page_header.tsx 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 00000000000000..52096a49435d72 --- /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/legacy/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/translations.ts rename to x-pack/plugins/uptime/public/pages/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/routes.tsx rename to x-pack/plugins/uptime/public/routes.tsx index b5e20ef8a70a9b..eb0587c0417a2b 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; +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'; 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 00000000000000..441a3cefdf204d --- /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 9a2db5be60b12a..524044f873687a 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 00000000000000..dee2df77707d28 --- /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 00000000000000..8ce4cf011406be --- /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 00000000000000..838e5b8246b4b5 --- /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 100% 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 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 00000000000000..c4ecb769abefce --- /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 00000000000000..acd9bec5a74bcb --- /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/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 100% 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 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 b0734cb5ccabbc..0aa85609fe4f06 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/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index.ts rename to x-pack/plugins/uptime/public/state/effects/index.ts 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/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index.ts rename to x-pack/plugins/uptime/public/state/reducers/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts rename to x-pack/plugins/uptime/public/state/reducers/index_pattern.ts index bc482e2f35c451..b357f5a904ea63 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts @@ -5,7 +5,7 @@ */ import { handleActions, Action } from 'redux-actions'; import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; export interface IndexPatternState { index_pattern: IIndexPattern | null; 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 00000000000000..61e03a95929211 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.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 { 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 { 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(), +}; + +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/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 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 00000000000000..c81ee6875f305a --- /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/legacy/plugins/uptime/public/state/reducers/utils.ts b/x-pack/plugins/uptime/public/state/reducers/utils.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts rename to x-pack/plugins/uptime/public/state/reducers/utils.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts rename to x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/selectors/index.ts rename to x-pack/plugins/uptime/public/state/selectors/index.ts 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 92775a26638633..0d18f959230d11 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 725b53aeca02db..d68bbabe82b861 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 98c6be5aa3c8e8..f4d1c72770494d 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 0176471aec1be2..46f46720d4c04a 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 4f4c6e3011ad14..24da3f3fa4d067 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 { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +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 }; 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 829e6f92d37020..f9df559a3977ba 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 fb44f5727aab38..26515fb4b4c63b 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 b49a6b22ff9768..894e2316dc927f 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,7 +5,7 @@ */ import { getCerts } from '../get_certs'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getCerts', () => { let mockHits: any; 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 03e2bc7a44bd04..75bf5096bd9978 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,7 +5,7 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; 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 5d3f9ce8d4ad9d..45be1df3e8d3bf 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 { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { 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 e47be617d7c994..82e624221c301c 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 { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; import { ScopedClusterClient } from 'src/core/server'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; interface BucketItemCriteria { monitor_id: string; 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 4de7d3ffd2a7d5..e456670a5e68de 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 { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getPingHistogram', () => { const standardMockResponse: any = { 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 abd3655cc64028..fd890a30cf7428 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 { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getAll', () => { let mockEsSearchResult: any; 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 4f99fbe94d54c7..b427e7cae1a7e8 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Cert, GetCertsParams } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { Cert, GetCertsParams } from '../../../common/runtime_types'; export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({ callES, 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 95d23ddcbf466e..dbe71cf689214f 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 6f7854d35b308b..7688f04f1acd95 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 a8e9ccb875a08c..98ce449002f217 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 */ 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 4ce7176b57b199..cf4ffa339ddfc5 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 e9c745b0a87133..ea2a7e790652bd 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 f49e404ffb084b..c8d3ca043edc50 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 4b40943a857051..b1791dd04861c4 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 5a8927764ea5c2..299913c8dff086 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 6eccfdb13cef74..a6a0e3c3d6542e 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 01f2ad88161cf0..b57bc87d454183 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 2a8f681ab34536..d4ad80c85ec3d5 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 84774cdeed8567..e53fff429dd8df 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 47034c21301164..40775bde1c7f5a 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 4739c804d24e75..d21259fad77a66 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, STATES } 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 ( 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 84167840d5d9ba..bef8106ad1896c 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 3449febfa5b053..e60c52660915a1 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 31d9166eb1e73b..2fb95620282583 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 43fc54fb258089..977c32ad1f984b 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 2ec52d400b5972..35e9647196454e 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 84154429b91888..69507d2950cd84 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -8,10 +8,17 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { HistogramResult, Ping, - PingsResponse as PingResults, + PingsResponse, GetCertsParams, GetPingsParams, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; + Cert, + OverviewFilters, + MonitorDetails, + MonitorLocations, + Snapshot, + StatesIndexStatus, +} from '../../../common/runtime_types'; +import { MonitorDurationResult } from '../../../common/types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -23,17 +30,8 @@ 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>; @@ -47,7 +45,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 d849fbd8ce0a8f..28b9eaad2cf6f0 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../common/constants'; +import { DynamicSettings } from '../../common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 7cc591a6b2db17..13d1ae216f2041 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 index 31fb3f4ab96a78..f2e1700b23e7d8 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs.ts @@ -7,7 +7,7 @@ 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'; +import { API_URLS } from '../../common/constants'; const DEFAULT_INDEX = 0; const DEFAULT_SIZE = 25; 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 3f4e2fc345182c..31833a25ee8acf 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_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 689a75c5903a64..26715f0ff37b64 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 8ed73d90b2389e..9a4280efa98f92 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 5cb4e8a6241b74..60b3eafaa765e6 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 66ce9871506d4e..a110209043a7e7 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 9cf1340fb94090..bb002f8a8c286c 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 1cc010781457e2..69e719efb0719d 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 9743ced13350a4..34313211061b08 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 deac05f36c8dc9..00cbaf0d16723c 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 dceef5ecb78486..41078f735920b8 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 80a887a7f64a9d..d97195a7fe2b17 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 d870f492801171..7809e102a499f8 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 4b2db71037071e..d8387e79e9089b 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 e05e7a4d7faf1a..8720b9dc60b12a 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 974f3eb6db60ff..efe1f859059706 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 a7551ad7e2fadd..1244657ed99889 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 46258e41d5d69d..4151deab45213d 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 338610e9243a40..bae6dada48fb7e 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 457b7621e84bd6..870ed3cf0cc0fc 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; @@ -75,7 +75,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 +123,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 05139213b76b9c..400aec7e11c8de 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 019b15cc1862a4..00000000000000 --- 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 00000000000000..45edd4c092da97 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/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 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'], + 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/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 100% 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 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 fe0f630830a561..1a47addf36ab33 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 29708f86b0a9b0..ac32f05805e4a3 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/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index eeb0818b5fbabd..4c76ebfb93b0bd 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 054f8f6141817e..399ae0f27f5b1a 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,7 +11,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 "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts 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 e00589b7e85b7b..386254e49c19c8 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 fd996ea4507bab..9b66326fa6157a 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 a58e14dd563ef8..af8af72d458fd2 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/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index d8e4f808f5cd23..59cf22b52920cc 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 5122a74d53b725..112149a32649a2 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 3faa54ee0b2190..715573ef1237e6 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/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 0d1596a95bfbbd..95ccfb897cf546 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/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index e8d336e875b991..73fe435764b74c 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 f08ce33d8b60f0..adde6dd184b817 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 cf0641acf9e1cd..577299e6526102 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 ca51676126e73d..b405b5065bc0e3 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 d8e9749744ea49..c934ddf8a406bf 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 d33b92acf95a54..5b8e03269ceefb 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -40,18 +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_id = outputAPIKeyBody.id; - 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, diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index 8bb3475da6cc9d..28a317893f5b29 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 3c12f5e4dc7893..991dc4a7f96cfe 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 00000000000000..73d59bcdcd9a49 --- /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/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts index bbc766df34dcfe..10857caab98e27 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -91,12 +91,11 @@ export default ({ getService }: FtrProviderContext) => { model_plot_config: { enabled: true }, }, expected: { - responseCode: 403, + responseCode: 404, responseBody: { - statusCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + statusCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts index 6a57db1687868f..a5cb68d7821267 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -197,8 +197,8 @@ export default ({ getService }: FtrProviderContext) => { requestBody: {}, // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. expected: { - responseCode: 403, - error: 'Forbidden', + responseCode: 404, + error: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 23ddd3b63a2eff..c42fc95c1bc7f2 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -84,10 +84,9 @@ export default ({ getService }: FtrProviderContext) => { startDatafeed: false, }, expected: { - responseCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + responseCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index ef7e48388ff660..fcdf268ff27b0a 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 cf9d8d8c9a5155..b89a1448d5fe6f 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 6e74375cedc126..0a2ee9c82bce22 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 bec31a018cac8d..2f1a792aff25bf 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 02152c0b71ca9e..30f9f6f04a2423 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 9e43405b324faa..2303b9ecfb78fd 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 6b12b4e3c938ab..22e133e48bbd2b 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 13e98f09d072b6..1eba41e238c819 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 ee4344bb0f1ee7..6ab7945e9000d6 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 7e5cbd7673af7f..95dbb44e30c41c 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 ce2f8d4a0b5e5f..ef7d82d2ea8d96 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 5aa7a10e07c2a4..75670374b6f632 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 f1e926b3ede426..39055e971d1185 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 6fe11bc2947950..2d9f576ef37e95 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 e0db91449a8ccd..2338d4ce45c8db 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 a6ab4493448f33..de57b0c3f469fb 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 5d1e645bcf80be..f88d5355f22c1c 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 8467308d709af6..e4e8b5db3d7e38 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 b463c4db996536..c9674e740f76d9 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 f1869d40fbf1d5..c8ea1be7d3f11d 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 6d125807e169d1..6c566ec7cb23b8 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 7510ea3f34d282..a3a15d8f8b0143 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) { 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 5258426cf193c1..f343cd1da8788b 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 ea980721b831bb..95caf50d1ca7a4 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 @@ -7,9 +7,8 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { DynamicSettingsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; - +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'); 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 3c17370532f915..c3d5849e028ab0 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 f1e37bff405fd3..c5a691312f5250 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 a261763d5991f9..3d754d89cf9be0 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 017ef02afe5ea7..99e09aa5ce886a 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/services/index.ts b/x-pack/test/api_integration/services/index.ts index 84b8476bd1dd10..6dcc9bb291b02a 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 00000000000000..851720895c6201 --- /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 7d6aa5e8a9dd56..ba60e66682955b 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/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index e89352118990a1..1e6600c7cd2c0d 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/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 1c83a17e78ca7d..abfd90bd6ad8e4 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/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index a240a55b9765c6..df41b725f6daf5 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 eef47ae0341c84..8d3b98004245ea 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 794bd2f1f0db36..23825836caad32 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 00000000000000..3a2b8f5cc783f9 --- /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 00000000000000..796e15b4e270f4 --- /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 00000000000000..b2eb645c8372c0 --- /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/index.ts b/x-pack/test/functional/apps/discover/index.ts index 2e5bdd736337d7..7688315a005168 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 00000000000000..7a33e7f5135d48 --- /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/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 994f65a79011ee..09d246a3a6e1b5 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 c7ad3f933ea64a..b5d43be21f0c52 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 c6b7517fc1858e..046b8ec44b9fa5 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 0c1d57202b8eb3..ed8bec570ab60a 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 a287d53d5df0bc..89ed51f65b9306 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 c703738e37228a..3dd1592286a3af 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 95228a520aaa25..ee4b59d04899fa 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 f40c908f23c809..7293405ce80ffb 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 d334fa7956be47..1c03b3ebf6681b 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/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index ee710b00b0be8b..0ca09aa317bc0a 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 a5faf325aa6cb9..a9133bb3801798 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 8827559a5f4709..ae88208fa9a118 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 9b5ae171d41159..b8ac646c35875d 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 570deee01c6843..d3934d674124e2 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 4739f987541d6b..f6a9c96492f394 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 0279c70bb73a92..547c489411b5f4 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 a5652d76358eb4..b1fee1633641ad 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 43053decb39247..0f8655e3c6bbca 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 cc7c9828ce87d7..9e1829998bec4c 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 8a6741bd88daac..eb57a743f14cb2 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 d98d8feaaf4fed..93f225989592ed 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 8dfe058cf6885e..4e8a9000598b3c 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 271f3e2018dad8..c3718e421d4517 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 ae958ad7f570fc..868e75228f1ccd 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 e71b57a4562e77..e4e0c1c92d73aa 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 95930f18061fab..35536b4eeda126 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 879a0a11ede78a..394e48404b927a 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 00000000000000..07306d8fb29560 --- /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 00000000000000..5b2edad9e6d95f --- /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/security.ts b/x-pack/test/functional/apps/security/security.ts index 2096a7755e01de..37516acec7c4b8 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 a007c40a06b626..b4adfe1ce2b66e 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 f49a74a661a63a..04d59334a01c47 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 608c7f321a08f0..56ebee79d06fff 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 38220c15cb266b..d45b8a1ea4cdb7 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 7b4a1e6e2b8a08..b88311597d7657 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 58551aaaf41127..fa5a0c0aa04029 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 e6e12f60f0bcca..4f71d81eacdf80 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 dbdbca0368146a..0e61635fb70e4f 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'); 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 fd5673de0d7a7c..26aec913e47562 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'); 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 42118f4c63b1fe..d2210bee0063d1 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/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 64b6300e0df631..7a813a5cdfb526 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; -import { 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) => { diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts index f30367ba3dd0b3..1c23b8cde86064 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 00000000000000..5ef954e334d815 --- /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 9f0d5a5de405a0..e894a890fdb501 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 f26110513a9b33..2c6238704bea0f 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -57,6 +57,8 @@ export default async function({ readConfigFile }) { 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'), ], @@ -196,6 +198,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 +234,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 8334749a696d7d..00000000000000 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/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 3d41096490a36a..d22e3cd3fecdda 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -1,11 +1,11 @@ { "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", @@ -21,11 +21,11 @@ { "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", @@ -40,11 +40,11 @@ { "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", @@ -59,11 +59,11 @@ { "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", @@ -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 edc6f8d58d300d..409cc3c689eafc 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" @@ -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 00000000000000..34a30bd84a5929 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 00000000000000..20f8f840ee8631 --- /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/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 782d57adea7707..4b8c2944ef190f 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page'; import { GrokDebuggerPageProvider } from './grok_debugger_page'; // @ts-ignore not ts yet import { WatcherPageProvider } from './watcher_page'; -// @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 1bf637c50b0ba4..57b2847cc2e500 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 cdfafeec1bf468..00000000000000 --- 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 00000000000000..2c20519a8d2140 --- /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/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index 0e638963f2367d..e2451328ea9414 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/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 9719152b62d354..96f5e45ce2ca47 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../plugins/uptime/common/runtime_types'; export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); 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 bbf8881f0c62a9..597f1ad9119b09 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'); 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 2c29954528bd59..d0ce18bbc1c54c 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() { @@ -148,6 +149,56 @@ 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 + ); + // 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 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); + }); + }); + describe('View In App', function() { const testRunUuid = uuid.v4(); @@ -453,6 +504,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 +520,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/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index a620b1d9533767..71b22a336f6b92 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -50,8 +50,6 @@ 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.preconfigured=${JSON.stringify([ { id: 'my-slack1', 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 5b506c20e029c0..2a0d28f2467655 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 c581e0c246e132..adb31f3562a6f7 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/feature_usage_test/kibana.json b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json new file mode 100644 index 00000000000000..b11b7ada24a578 --- /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 00000000000000..e07915ab5f46b5 --- /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 00000000000000..b36d6dca077f74 --- /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 00000000000000..494bcdbf5f61eb --- /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 00000000000000..a8225838fd9bfc --- /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 00000000000000..416ef7fa345919 --- /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 00000000000000..c8d47decd94c1c --- /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 00000000000000..77233f463734a1 --- /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 00000000000000..1fee2decbcba94 --- /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 00000000000000..508d58b8f0ca96 --- /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 e5b645367b8b71..00000000000000 --- 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 785fbed3414237..00000000000000 --- 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 ec63c512e9cd79..00000000000000 --- 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 d664357c3ba126..e5b840b335846f 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'; @@ -43,10 +43,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 +80,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 +101,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,29 +193,12 @@ 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: [ 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 2de395308ce74c..31668e8345275e 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,7 +98,7 @@ 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 }] }, 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 00000000000000..41f2cfc7983ef0 --- /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 00000000000000..6cafb60bf81679 --- /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 e8f976d5ae6e30..00cefa42711c9c 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 fc5147ad8c4544..00000000000000 --- 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 753d2b2a20ab95..00000000000000 --- 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 be3ac09bdec10b..00000000000000 --- 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 16b1008c645a05..00000000000000 --- 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 e7a08753b591f5..00000000000000 --- 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 c1a2ae634662ca..00000000000000 --- a/x-pack/test/reporting/functional/reporting.js +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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 filterBar = getService('filterBar'); - 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('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); - }); - }); - }); - - 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/yarn.lock b/yarn.lock index b9f80cd5923b7c..94e6a0a11aa993 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1192,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" @@ -1236,10 +1262,10 @@ dependencies: "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.4.1": - version "18.4.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.1.tgz#19d82c39aef347fd00b33e33b68b683ac4d745b0" - integrity sha512-vV5AAKIKbwgY923OD2Rsr77XFHmsUsWWg/aeCZvG5/b6Yb+fNgM0RF94GADiDMvRvQANhTn2CPPVvNfL18MegQ== +"@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" @@ -1538,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" @@ -1557,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" @@ -3704,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" @@ -3744,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" @@ -4181,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== @@ -4198,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== @@ -4287,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" @@ -4347,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== @@ -4364,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" @@ -4749,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" @@ -4756,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" @@ -5506,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" @@ -6417,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" @@ -6453,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" @@ -6684,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== @@ -6779,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== @@ -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.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== @@ -10409,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" @@ -10429,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" @@ -10438,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" @@ -10752,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" @@ -13000,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" @@ -13022,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" @@ -13243,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== @@ -14947,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" @@ -16377,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" @@ -17319,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" @@ -17649,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" @@ -18850,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" @@ -19759,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== @@ -19820,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" @@ -20186,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" @@ -20661,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" @@ -20775,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" @@ -20801,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" @@ -22140,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" @@ -25139,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== @@ -25693,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" @@ -25900,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" @@ -26258,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" @@ -26976,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" @@ -27928,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" @@ -30093,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" @@ -30665,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== @@ -31943,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" @@ -32567,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"