diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac..c474998e6fd3d 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 180) { +kibanaPipeline(timeoutMinutes: 240) { catchErrors { withEnv([ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. diff --git a/.eslintignore b/.eslintignore index 1f22b6074e76e..2ed9ecf971ff3 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 dfb4603ba95af..c9b41ec711b7f 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 3e46543246ccf..c17538849660e 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 @@ -210,22 +206,21 @@ /x-pack/plugins/ingest_pipelines/ @elastic/es-ui # Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team +/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem # SIEM -/x-pack/legacy/plugins/siem/ @elastic/siem -/x-pack/plugins/siem/ @elastic/siem -/x-pack/test/detection_engine_api_integration @elastic/siem -/x-pack/test/api_integration/apis/siem @elastic/siem -/x-pack/plugins/case @elastic/siem +/x-pack/plugins/siem/ @elastic/siem @elastic/endpoint-app-team +/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team +/x-pack/test/api_integration/apis/siem @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team # Security Intelligence And Analytics -/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics /x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/.i18nrc.json b/.i18nrc.json index 35ce745234346..b04c02f6b2265 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 9b31f3fae6d16..44b4d49384136 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -2,8 +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' + - '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 98cc6994f229b..958ced8c6195a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,54 +4,56 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { - githubPr.withDefaultPrComments { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-siemCypress': { processNumber -> - whenChanged(['x-pack/plugins/siem/', 'x-pack/legacy/plugins/siem/', 'x-pack/test/siem_cypress/']) { - kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) - } - }, + ciStats.trackBuild { + githubPr.withDefaultPrComments { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-siemCypress': { processNumber -> + whenChanged(['x-pack/plugins/siem/', 'x-pack/test/siem_cypress/']) { + kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) + } + }, - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) + } } - } - retryable.printFlakyFailures() - kibanaPipeline.sendMail() + retryable.printFlakyFailures() + kibanaPipeline.sendMail() + } } diff --git a/docs/developer/core-development.asciidoc b/docs/developer/core-development.asciidoc index 447a4c6d2601b..8f356abd095f2 100644 --- a/docs/developer/core-development.asciidoc +++ b/docs/developer/core-development.asciidoc @@ -7,6 +7,7 @@ * <> * <> * <> +* <> include::core/development-basepath.asciidoc[] @@ -19,3 +20,5 @@ include::core/development-elasticsearch.asciidoc[] include::core/development-unit-tests.asciidoc[] include::core/development-functional-tests.asciidoc[] + +include::core/development-es-snapshots.asciidoc[] diff --git a/docs/developer/core/development-es-snapshots.asciidoc b/docs/developer/core/development-es-snapshots.asciidoc new file mode 100644 index 0000000000000..4cd4f31e582db --- /dev/null +++ b/docs/developer/core/development-es-snapshots.asciidoc @@ -0,0 +1,113 @@ +[[development-es-snapshots]] +=== Daily Elasticsearch Snapshots + +For local development and CI, Kibana, by default, uses Elasticsearch snapshots that are built daily when running tasks that require Elasticsearch (e.g. functional tests). + +A snapshot is just a group of tarballs, one for each supported distribution/architecture/os of Elasticsearch, and a JSON-based manifest file containing metadata about the distributions. + +https://ci.kibana.dev/es-snapshots[A dashboard] is available that shows the current status and compatibility of the latest Elasticsearch snapshots. + +==== Process Overview + +1. Elasticsearch snapshots are built for each current tracked branch of Kibana. +2. Each snapshot is uploaded to a public Google Cloud Storage bucket, `kibana-ci-es-snapshots-daily`. +** At this point, the snapshot is not automatically used in CI or local development. It needs to be tested/verified first. +3. Each snapshot is tested with the latest commit of the corresponding Kibana branch, using the full CI suite. +4. After CI +** If the snapshot passes, it is promoted and automatically used in CI and local development. +** If the snapshot fails, the issue must be investigated and resolved. A new incompatibility may exist between Elasticsearch and Kibana. + +==== Using the latest snapshot + +When developing locally, you may wish to use the most recent Elasticsearch snapshot, even if it's failing CI. To do so, prefix your commands with the follow environment variable: + +["source","bash"] +----------- +KBN_ES_SNAPSHOT_USE_UNVERIFIED=true +----------- + +You can use this flag with any command that downloads and runs Elasticsearch snapshots, such as `scripts/es` or the FTR. + +For example, to run functional tests with the latest snapshot: + +["source","bash"] +----------- +KBN_ES_SNAPSHOT_USE_UNVERIFIED=true node scripts/functional_tests_server +----------- + +===== For Pull Requests + +Currently, there is not a way to run your pull request with the latest unverified snapshot without a code change. You can, however, do it with a small code change. + +1. Edit `Jenkinsfile` in the root of the Kibana repo +2. Add `env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 'true'` at the top of the file. +3. Commit the change + +Your pull request should then use the latest snapshot the next time that it runs. Just don't merge the change to `Jenkinsfile`! + +==== Google Cloud Storage buckets + +===== kibana-ci-es-snapshots-daily + +This bucket stores snapshots that are created on a daily basis, and is the primary location used by `kbn-es` to download snapshots. + +Snapshots are automatically deleted after 10 days. + +The file structure for this bucket looks like this: + +* `/manifest-latest.json` +* `/manifest-latest-verified.json` +* `/archives//*.tar.gz` +* `/archives//*.tar.gz.sha512` +* `/archives//manifest.json` + +===== kibana-ci-es-snapshots-permanent + +This bucket stores only the most recently promoted snapshot for each version. Old snapshots are only deleted when new ones are uploaded. + +This bucket serves as permanent snapshot storage for old branches/versions that are no longer being built. `kbn-es` checks the daily bucket first, followed by this one if no snapshots were found. + +The file structure for this bucket looks like this: + +* `/*.tar.gz` +* `/*.tar.gz.sha512` +* `/manifest.json` + +==== How snapshots are built, tested, and promoted + +Each day, a https://kibana-ci.elastic.co/job/elasticsearch+snapshots+trigger/[Jenkins job] runs that triggers Elasticsearch builds for each currently tracked branch/version. This job is automatically updated with the correct branches whenever we release new versions of Kibana. + +===== Build + +https://kibana-ci.elastic.co/job/elasticsearch+snapshots+build/[This Jenkins job] builds the Elasticsearch snapshots and uploads them to GCS. + +The Jenkins job pipeline definition is https://github.com/elastic/kibana/blob/master/.ci/es-snapshots/Jenkinsfile_build_es[in the kibana repo]. + +1. Checkout Elasticsearch repo for the given branch/version. +2. Run `./gradlew -p distribution/archives assemble --parallel` to create all of the Elasticsearch distributions. +3. Create a tarball for each distribution. +4. Create a manifest JSON file containing info about the distribution, as well as its download URL. +5. Upload the tarballs and manifest to a unique location in the GCS bucket `kibana-ci-es-snapshots-daily`. +** e.g. `/archives/` +6. Replace `/manifest-latest.json` in GCS with this newest manifest. +** This allows the `KBN_ES_SNAPSHOT_USE_UNVERIFIED` flag to work. +7. Trigger the verification job, to run the full Kibana CI test suite with this snapshot. + +===== Verification and Promotion + +https://kibana-ci.elastic.co/job/elasticsearch+snapshots+verify/[This Jenkins job] tests the latest Elasticsearch snapshot with the full Kibana CI pipeline, and promotes if it there are no test failures. + +The Jenkins job pipeline definition is https://github.com/elastic/kibana/blob/master/.ci/es-snapshots/Jenkinsfile_verify_es[in the kibana repo]. + +1. Checkout Kibana and set up CI environment as normal. +2. Set the `ES_SNAPSHOT_MANIFEST` env var to point to the latest snapshot manifest. +3. Run CI (functional tests, integration tests, etc). +4. After CI +** If there was a test failure or other build error, send out an e-mail notification and stop. +** If there were no errors, promote the snapshot. + +Promotion is done as part of the same pipeline: + +1. Replace the manifest at `kibana-ci-es-snapshots-daily//manifest-latest-verified.json` with the manifest from the tested snapshot. +** At this point, the snapshot has been promoted and will automatically be used in CI and in local development. +2. Replace the snapshot at `kibana-ci-es-snapshots-permanent//` with the tested snapshot by copying all of the tarballs and the manifest file. \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md index 96f1143630856..dbc7d0ca431ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md @@ -19,5 +19,6 @@ export interface SavedObjectsCoreFieldMapping | [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean | | | [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
};
} | | | [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean | | +| [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) | number | boolean | string | | | [type](./kibana-plugin-core-server.savedobjectscorefieldmapping.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md new file mode 100644 index 0000000000000..627ea3695383a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) + +## SavedObjectsCoreFieldMapping.null\_value property + +Signature: + +```typescript +null_value?: number | boolean | string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md new file mode 100644 index 0000000000000..43ff9a930b974 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [$$spec](./kibana-plugin-plugins-data-public.field.__spec.md) + +## Field.$$spec property + +Signature: + +```typescript +$$spec: FieldSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md new file mode 100644 index 0000000000000..c3b2ac8d30b5a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [(constructor)](./kibana-plugin-plugins-data-public.field._constructor_.md) + +## Field.(constructor) + +Constructs a new instance of the `Field` class + +Signature: + +```typescript +constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable?: boolean); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| spec | FieldSpec | Field | | +| shortDotsEnable | boolean | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md new file mode 100644 index 0000000000000..fcfd7d73c8b0c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [aggregatable](./kibana-plugin-plugins-data-public.field.aggregatable.md) + +## Field.aggregatable property + +Signature: + +```typescript +aggregatable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md new file mode 100644 index 0000000000000..21b6917c4aad4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [conflictDescriptions](./kibana-plugin-plugins-data-public.field.conflictdescriptions.md) + +## Field.conflictDescriptions property + +Signature: + +```typescript +conflictDescriptions?: Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md new file mode 100644 index 0000000000000..4f51d88a3046e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [count](./kibana-plugin-plugins-data-public.field.count.md) + +## Field.count property + +Signature: + +```typescript +count?: number; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md new file mode 100644 index 0000000000000..0846a7595cf90 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [displayName](./kibana-plugin-plugins-data-public.field.displayname.md) + +## Field.displayName property + +Signature: + +```typescript +displayName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md new file mode 100644 index 0000000000000..efe1bceb43361 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [esTypes](./kibana-plugin-plugins-data-public.field.estypes.md) + +## Field.esTypes property + +Signature: + +```typescript +esTypes?: string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md new file mode 100644 index 0000000000000..fd7be589e87a7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [filterable](./kibana-plugin-plugins-data-public.field.filterable.md) + +## Field.filterable property + +Signature: + +```typescript +filterable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md new file mode 100644 index 0000000000000..431e043d1fecc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [format](./kibana-plugin-plugins-data-public.field.format.md) + +## Field.format property + +Signature: + +```typescript +format: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md new file mode 100644 index 0000000000000..59420747e0958 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [indexPattern](./kibana-plugin-plugins-data-public.field.indexpattern.md) + +## Field.indexPattern property + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md new file mode 100644 index 0000000000000..d51857090356f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [lang](./kibana-plugin-plugins-data-public.field.lang.md) + +## Field.lang property + +Signature: + +```typescript +lang?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md new file mode 100644 index 0000000000000..86ff2b2c28ae9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) + +## Field class + +Signature: + +```typescript +export declare class Field implements IFieldType +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(indexPattern, spec, shortDotsEnable)](./kibana-plugin-plugins-data-public.field._constructor_.md) | | Constructs a new instance of the Field class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [$$spec](./kibana-plugin-plugins-data-public.field.__spec.md) | | FieldSpec | | +| [aggregatable](./kibana-plugin-plugins-data-public.field.aggregatable.md) | | boolean | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.field.conflictdescriptions.md) | | Record<string, string[]> | | +| [count](./kibana-plugin-plugins-data-public.field.count.md) | | number | | +| [displayName](./kibana-plugin-plugins-data-public.field.displayname.md) | | string | | +| [esTypes](./kibana-plugin-plugins-data-public.field.estypes.md) | | string[] | | +| [filterable](./kibana-plugin-plugins-data-public.field.filterable.md) | | boolean | | +| [format](./kibana-plugin-plugins-data-public.field.format.md) | | any | | +| [indexPattern](./kibana-plugin-plugins-data-public.field.indexpattern.md) | | IndexPattern | | +| [lang](./kibana-plugin-plugins-data-public.field.lang.md) | | string | | +| [name](./kibana-plugin-plugins-data-public.field.name.md) | | string | | +| [script](./kibana-plugin-plugins-data-public.field.script.md) | | string | | +| [scripted](./kibana-plugin-plugins-data-public.field.scripted.md) | | boolean | | +| [searchable](./kibana-plugin-plugins-data-public.field.searchable.md) | | boolean | | +| [sortable](./kibana-plugin-plugins-data-public.field.sortable.md) | | boolean | | +| [subType](./kibana-plugin-plugins-data-public.field.subtype.md) | | IFieldSubType | | +| [type](./kibana-plugin-plugins-data-public.field.type.md) | | string | | +| [visualizable](./kibana-plugin-plugins-data-public.field.visualizable.md) | | boolean | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md new file mode 100644 index 0000000000000..d2a9b9b86aefc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [name](./kibana-plugin-plugins-data-public.field.name.md) + +## Field.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md new file mode 100644 index 0000000000000..676ff9bdfc35a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [script](./kibana-plugin-plugins-data-public.field.script.md) + +## Field.script property + +Signature: + +```typescript +script?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md new file mode 100644 index 0000000000000..1f6c8105e3f61 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [scripted](./kibana-plugin-plugins-data-public.field.scripted.md) + +## Field.scripted property + +Signature: + +```typescript +scripted?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md new file mode 100644 index 0000000000000..186d344f50378 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [searchable](./kibana-plugin-plugins-data-public.field.searchable.md) + +## Field.searchable property + +Signature: + +```typescript +searchable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md new file mode 100644 index 0000000000000..0cd4b14d0e1e5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [sortable](./kibana-plugin-plugins-data-public.field.sortable.md) + +## Field.sortable property + +Signature: + +```typescript +sortable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md new file mode 100644 index 0000000000000..bef3b2131fa47 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [subType](./kibana-plugin-plugins-data-public.field.subtype.md) + +## Field.subType property + +Signature: + +```typescript +subType?: IFieldSubType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md new file mode 100644 index 0000000000000..490615edcf097 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [type](./kibana-plugin-plugins-data-public.field.type.md) + +## Field.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md new file mode 100644 index 0000000000000..f32a5c456dc5d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [visualizable](./kibana-plugin-plugins-data-public.field.visualizable.md) + +## Field.visualizable property + +Signature: + +```typescript +visualizable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md new file mode 100644 index 0000000000000..e38da6600696c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [(constructor)](./kibana-plugin-plugins-data-public.fieldformat._constructor_.md) + +## FieldFormat.(constructor) + +Constructs a new instance of the `FieldFormat` class + +Signature: + +```typescript +constructor(_params?: IFieldFormatMetaParams, getConfig?: FieldFormatsGetConfigFn); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| \_params | IFieldFormatMetaParams | | +| getConfig | FieldFormatsGetConfigFn | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md new file mode 100644 index 0000000000000..ac3f256a9afc3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [\_params](./kibana-plugin-plugins-data-public.fieldformat._params.md) + +## FieldFormat.\_params property + +Signature: + +```typescript +protected readonly _params: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md new file mode 100644 index 0000000000000..0535585cb4718 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [convert](./kibana-plugin-plugins-data-public.fieldformat.convert.md) + +## FieldFormat.convert() method + +Convert a raw value to a formatted string + +Signature: + +```typescript +convert(value: any, contentType?: FieldFormatsContentType, options?: HtmlContextTypeOptions | TextContextTypeOptions): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| value | any | | +| contentType | FieldFormatsContentType | | +| options | HtmlContextTypeOptions | TextContextTypeOptions | | + +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md new file mode 100644 index 0000000000000..436124ac08387 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [convertObject](./kibana-plugin-plugins-data-public.fieldformat.convertobject.md) + +## FieldFormat.convertObject property + + {FieldFormatConvert} have to remove the private because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +convertObject: FieldFormatConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md new file mode 100644 index 0000000000000..1d109a599d2d9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [fieldType](./kibana-plugin-plugins-data-public.fieldformat.fieldtype.md) + +## FieldFormat.fieldType property + + {string} - Field Format Type + +Signature: + +```typescript +static fieldType: string | string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md new file mode 100644 index 0000000000000..ec497de59d236 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [from](./kibana-plugin-plugins-data-public.fieldformat.from.md) + +## FieldFormat.from() method + +Signature: + +```typescript +static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| convertFn | FieldFormatConvertFunction | | + +Returns: + +`FieldFormatInstanceType` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md new file mode 100644 index 0000000000000..446e0c237ce13 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) + +## FieldFormat.getConfig property + +Signature: + +```typescript +protected getConfig: FieldFormatsGetConfigFn | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md new file mode 100644 index 0000000000000..f4eeb5eed06a0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getConverterFor](./kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md) + +## FieldFormat.getConverterFor() method + +Get a convert function that is bound to a specific contentType + +Signature: + +```typescript +getConverterFor(contentType?: FieldFormatsContentType): FieldFormatConvertFunction; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contentType | FieldFormatsContentType | | + +Returns: + +`FieldFormatConvertFunction` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md new file mode 100644 index 0000000000000..59afdc25df350 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getParamDefaults](./kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md) + +## FieldFormat.getParamDefaults() method + +Get parameter defaults {object} - parameter defaults + +Signature: + +```typescript +getParamDefaults(): Record; +``` +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md new file mode 100644 index 0000000000000..945ac7ededff6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [htmlConvert](./kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md) + +## FieldFormat.htmlConvert property + + {htmlConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +htmlConvert: HtmlContextTypeConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md new file mode 100644 index 0000000000000..91c3ff4f2d9a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [id](./kibana-plugin-plugins-data-public.fieldformat.id.md) + +## FieldFormat.id property + + {string} - Field Format Id + +Signature: + +```typescript +static id: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md new file mode 100644 index 0000000000000..c6afa27fe5952 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [isInstanceOfFieldFormat](./kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md) + +## FieldFormat.isInstanceOfFieldFormat() method + +Signature: + +```typescript +static isInstanceOfFieldFormat(fieldFormat: any): fieldFormat is FieldFormat; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldFormat | any | | + +Returns: + +`fieldFormat is FieldFormat` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md new file mode 100644 index 0000000000000..b53e301c46c1c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) + +## FieldFormat class + +Signature: + +```typescript +export declare abstract class FieldFormat +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(\_params, getConfig)](./kibana-plugin-plugins-data-public.fieldformat._constructor_.md) | | Constructs a new instance of the FieldFormat class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_params](./kibana-plugin-plugins-data-public.fieldformat._params.md) | | any | | +| [convertObject](./kibana-plugin-plugins-data-public.fieldformat.convertobject.md) | | FieldFormatConvert | undefined | {FieldFormatConvert} have to remove the private because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [fieldType](./kibana-plugin-plugins-data-public.fieldformat.fieldtype.md) | static | string | string[] | {string} - Field Format Type | +| [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) | | FieldFormatsGetConfigFn | undefined | | +| [htmlConvert](./kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md) | | HtmlContextTypeConvert | undefined | {htmlConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [id](./kibana-plugin-plugins-data-public.fieldformat.id.md) | static | string | {string} - Field Format Id | +| [textConvert](./kibana-plugin-plugins-data-public.fieldformat.textconvert.md) | | TextContextTypeConvert | undefined | {textConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [title](./kibana-plugin-plugins-data-public.fieldformat.title.md) | static | string | {string} - Field Format Title | +| [type](./kibana-plugin-plugins-data-public.fieldformat.type.md) | | any | {Function} - ref to child class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [convert(value, contentType, options)](./kibana-plugin-plugins-data-public.fieldformat.convert.md) | | Convert a raw value to a formatted string | +| [from(convertFn)](./kibana-plugin-plugins-data-public.fieldformat.from.md) | static | | +| [getConverterFor(contentType)](./kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md) | | Get a convert function that is bound to a specific contentType | +| [getParamDefaults()](./kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md) | | Get parameter defaults {object} - parameter defaults | +| [isInstanceOfFieldFormat(fieldFormat)](./kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md) | static | | +| [param(name)](./kibana-plugin-plugins-data-public.fieldformat.param.md) | | Get the value of a param. This value may be a default value. | +| [params()](./kibana-plugin-plugins-data-public.fieldformat.params.md) | | Get all of the params in a single object {object} | +| [setupContentType()](./kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md) | | | +| [toJSON()](./kibana-plugin-plugins-data-public.fieldformat.tojson.md) | | Serialize this format to a simple POJO, with only the params that are not default {object} | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md new file mode 100644 index 0000000000000..1e7fd9d161429 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [param](./kibana-plugin-plugins-data-public.fieldformat.param.md) + +## FieldFormat.param() method + +Get the value of a param. This value may be a default value. + +Signature: + +```typescript +param(name: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md new file mode 100644 index 0000000000000..5825af4925d06 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [params](./kibana-plugin-plugins-data-public.fieldformat.params.md) + +## FieldFormat.params() method + +Get all of the params in a single object {object} + +Signature: + +```typescript +params(): Record; +``` +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md new file mode 100644 index 0000000000000..41f5f2446f22a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [setupContentType](./kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md) + +## FieldFormat.setupContentType() method + +Signature: + +```typescript +setupContentType(): FieldFormatConvert; +``` +Returns: + +`FieldFormatConvert` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md new file mode 100644 index 0000000000000..57ccca9136081 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [textConvert](./kibana-plugin-plugins-data-public.fieldformat.textconvert.md) + +## FieldFormat.textConvert property + + {textConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +textConvert: TextContextTypeConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md new file mode 100644 index 0000000000000..b19246758f080 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [title](./kibana-plugin-plugins-data-public.fieldformat.title.md) + +## FieldFormat.title property + + {string} - Field Format Title + +Signature: + +```typescript +static title: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md new file mode 100644 index 0000000000000..5fa7d4841537b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [toJSON](./kibana-plugin-plugins-data-public.fieldformat.tojson.md) + +## FieldFormat.toJSON() method + +Serialize this format to a simple POJO, with only the params that are not default + + {object} + +Signature: + +```typescript +toJSON(): { + id: unknown; + params: _.Dictionary | undefined; + }; +``` +Returns: + +`{ + id: unknown; + params: _.Dictionary | undefined; + }` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md new file mode 100644 index 0000000000000..394a2e3cc9afb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [type](./kibana-plugin-plugins-data-public.fieldformat.type.md) + +## FieldFormat.type property + + {Function} - ref to child class + +Signature: + +```typescript +type: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md index 04a0d871cab2d..3969a97fa7789 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md @@ -7,7 +7,10 @@ Signature: ```typescript -export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; ``` ## Parameters @@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan | --- | --- | --- | | indexPattern | IIndexPattern | undefined | | | timeRange | TimeRange | | -| forceNow | Date | | +| options | {
forceNow?: Date;
fieldName?: string;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md new file mode 100644 index 0000000000000..c3998876c9712 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) + +## IIndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField?(): IFieldType | undefined; +``` +Returns: + +`IFieldType | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1bbd6cf67f0ce..1cb89822eb605 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -21,3 +21,9 @@ export interface IIndexPattern | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | | +## Methods + +| Method | Description | +| --- | --- | +| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md deleted file mode 100644 index f52a3324af36f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) - -## IndexPatternField.$$spec property - -Signature: - -```typescript -$$spec: FieldSpec; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md deleted file mode 100644 index cf7470c035a53..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [(constructor)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) - -## IndexPatternField.(constructor) - -Constructs a new instance of the `Field` class - -Signature: - -```typescript -constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable?: boolean); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indexPattern | IndexPattern | | -| spec | FieldSpec | Field | | -| shortDotsEnable | boolean | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md deleted file mode 100644 index 267c8f786b5dd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) - -## IndexPatternField.aggregatable property - -Signature: - -```typescript -aggregatable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md deleted file mode 100644 index 8e848276f21c4..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) - -## IndexPatternField.count property - -Signature: - -```typescript -count?: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md deleted file mode 100644 index ed9630f92fc97..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) - -## IndexPatternField.displayName property - -Signature: - -```typescript -displayName?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md deleted file mode 100644 index dec74df099d43..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) - -## IndexPatternField.esTypes property - -Signature: - -```typescript -esTypes?: string[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md deleted file mode 100644 index 4290c4a2f86b3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) - -## IndexPatternField.filterable property - -Signature: - -```typescript -filterable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md deleted file mode 100644 index d5df8ed628cb0..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) - -## IndexPatternField.format property - -Signature: - -```typescript -format: any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md deleted file mode 100644 index d1a1ee0905c6e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) - -## IndexPatternField.indexPattern property - -Signature: - -```typescript -indexPattern?: IndexPattern; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md deleted file mode 100644 index f731be8f613cf..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) - -## IndexPatternField.lang property - -Signature: - -```typescript -lang?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md deleted file mode 100644 index df0de6ce0e541..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ /dev/null @@ -1,40 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) - -## IndexPatternField class - -Signature: - -```typescript -export declare class Field implements IFieldType -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(indexPattern, spec, shortDotsEnable)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the Field class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) | | FieldSpec | | -| [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | -| [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | -| [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | -| [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | any | | -| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | -| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | -| [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | -| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | | -| [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | -| [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | -| [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | -| [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | IFieldSubType | | -| [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) | | string | | -| [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) | | boolean | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md deleted file mode 100644 index cb24621e73209..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) - -## IndexPatternField.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md deleted file mode 100644 index 132ba25a47637..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) - -## IndexPatternField.script property - -Signature: - -```typescript -script?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md deleted file mode 100644 index 1dd6bc865a75d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) - -## IndexPatternField.scripted property - -Signature: - -```typescript -scripted?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md deleted file mode 100644 index 42f984d851435..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) - -## IndexPatternField.searchable property - -Signature: - -```typescript -searchable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md deleted file mode 100644 index 72d225185140b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) - -## IndexPatternField.sortable property - -Signature: - -```typescript -sortable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md deleted file mode 100644 index 2d807f8a5739c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) - -## IndexPatternField.subType property - -Signature: - -```typescript -subType?: IFieldSubType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md deleted file mode 100644 index c8483c9b83c9a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) - -## IndexPatternField.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md deleted file mode 100644 index dd661ae779c11..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) - -## IndexPatternField.visualizable property - -Signature: - -```typescript -visualizable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 604ac5120922b..e1df493143b73 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) | | @@ -42,7 +43,7 @@ | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | | | [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | | -| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | | +| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | ## Interfaces diff --git a/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 4d7a0b3cfbbca..bd617990a00a2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -9,7 +9,7 @@ ```typescript setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").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/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7081590931a99..51910169e8673 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -142,7 +142,14 @@ This setting does not have an effect when loading a saved search. Highlighting slows requests when working on big documents. +[float] +[[kibana-ml-settings]] +==== Machine learning +[horizontal] +`ml:fileDataVisualizerMaxFileSize`:: Sets the file size limit when importing +data in the {data-viz}. The default value is `100MB`. The highest supported +value for this setting is `1GB`. [float] diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index b71e1c672756a..36578c909f513 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/ml/index.asciidoc b/docs/user/ml/index.asciidoc index e9ef4a55b2b3a..6483ddde07335 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 bfea8c7423f9c..0ad304fdf2f69 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "data/optimize", "built_assets", ".eslintcache", - ".node_binaries" + ".node_binaries", + "src/plugins/*/target" ] } }, diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index ee9f349f49051..08b2e5b226967 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -10,6 +10,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { + "axios": "^0.19.0", "chalk": "^2.4.2", "dedent": "^0.7.0", "execa": "^4.0.0", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md new file mode 100644 index 0000000000000..6133f9871699f --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md @@ -0,0 +1,23 @@ +# Kibana CI Stats reporter + +We're working on building a new service, the Kibana CI Stats service, which will collect information about CI runs and metrics produced while testing Kibana, and then provide tools for reporting on those metrics in specific PRs and overall as a way to spot trends. + +### `CiStatsReporter` + +This class integrates with the `ciStats.trackBuild {}` Jenkins Pipeline function, consuming the `KIBANA_CI_STATS_CONFIG` variable produced by that wrapper, and then allowing test code to report stats to the service. + +To create an instance of the reporter, import the class and call `CiStatsReporter.fromEnv(log)` (passing it a tooling log). + +#### `CiStatsReporter#metric(name: string, subName: string, value: number)` + +Use this method to record metrics in the Kibana CI Stats service. + +Example: + +```ts +import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; + +const log = new ToolingLog(...); +const reporter = CiStatsReporter.fromEnv(log) +reporter.metric('Build speed', specificBuildName, timeToRunBuild) +``` \ No newline at end of file diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts new file mode 100644 index 0000000000000..5fe1844a85563 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -0,0 +1,153 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { inspect } from 'util'; + +import Axios from 'axios'; + +import { ToolingLog } from '../tooling_log'; + +interface Config { + apiUrl: string; + apiToken: string; + buildId: string; +} + +function parseConfig(log: ToolingLog) { + const configJson = process.env.KIBANA_CI_STATS_CONFIG; + if (!configJson) { + log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); + return; + } + + let config: unknown; + try { + config = JSON.parse(configJson); + } catch (_) { + // handled below + } + + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config as { [k in keyof Config]: unknown }); + } + + log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); + return; +} + +function validateConfig(log: ToolingLog, config: { [k in keyof Config]: unknown }) { + const validApiUrl = typeof config.apiUrl === 'string' && config.apiUrl.length !== 0; + if (!validApiUrl) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api url, stats will not be reported'); + return; + } + + const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; + if (!validApiToken) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); + return; + } + + const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; + if (!validId) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); + return; + } + + return config as Config; +} + +export class CiStatsReporter { + static fromEnv(log: ToolingLog) { + return new CiStatsReporter(parseConfig(log), log); + } + + constructor(private config: Config | undefined, private log: ToolingLog) {} + + isEnabled() { + return !!this.config; + } + + async metric(name: string, subName: string, value: number) { + if (!this.config) { + return; + } + + let attempt = 0; + const maxAttempts = 5; + + while (true) { + attempt += 1; + + try { + await Axios.request({ + method: 'POST', + url: '/metric', + baseURL: this.config.apiUrl, + params: { + buildId: this.config.buildId, + }, + headers: { + Authorization: `token ${this.config.apiToken}`, + }, + data: { + name, + subName, + value, + }, + }); + + return; + } catch (error) { + if (!error?.request) { + // not an axios error, must be a usage error that we should notify user about + throw error; + } + + if (error?.response && error.response.status !== 502) { + // error response from service was received so warn the user and move on + this.log.warning( + `error recording metric [status=${error.response.status}] [resp=${inspect( + error.response.data + )}] [${name}/${subName}=${value}]` + ); + return; + } + + if (attempt === maxAttempts) { + this.log.warning( + `failed to reach kibana-ci-stats service too many times, unable to record metric [${name}/${subName}=${value}]` + ); + return; + } + + // we failed to reach the backend and we have remaining attempts, lets retry after a short delay + const reason = error?.response?.status + ? `${error.response.status} response` + : 'no response'; + + this.log.warning( + `failed to reach kibana-ci-stats service [reason=${reason}], retrying in ${attempt} seconds` + ); + + await new Promise(resolve => setTimeout(resolve, attempt * 1000)); + } + } + } +} diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts new file mode 100644 index 0000000000000..3487de08f034d --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './ci_stats_reporter'; diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 305e29a0e41df..40ee72d94729f 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -37,3 +37,4 @@ export { run, createFailError, createFlagError, combineErrors, isFailError, Flag export { REPO_ROOT } from './repo_root'; export { KbnClient } from './kbn_client'; export * from './axios'; +export * from './ci_stats_reporter'; diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index c0bb408d60d8f..e46075eff63a7 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -21,10 +21,11 @@ import 'source-map-support/register'; import Path from 'path'; -import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; +import { run, REPO_ROOT, createFlagError, createFailError, CiStatsReporter } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; +import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; run( @@ -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 8026cf39db73d..29922944e8817 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -21,3 +21,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; export * from './common/disallowed_syntax_plugin'; +export * from './report_optimizer_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts index 5dd500cd7a9e4..b4b02649259a2 100644 --- a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts +++ b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts @@ -17,20 +17,20 @@ * under the License. */ -import * as Rx from 'rxjs'; import { tap } from 'rxjs/operators'; import { createFailError } from '@kbn/dev-utils'; -import { pipeClosure, Update } from '../common'; +import { pipeClosure } from '../common'; +import { OptimizerUpdate$ } from '../run_optimizer'; import { OptimizerState } from './optimizer_state'; import { OptimizerConfig } from './optimizer_config'; export function handleOptimizerCompletion(config: OptimizerConfig) { - return pipeClosure((source$: Rx.Observable>) => { + return pipeClosure((update$: OptimizerUpdate$) => { let prevState: OptimizerState | undefined; - return source$.pipe( + return update$.pipe( tap({ next: update => { prevState = update.state; diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts new file mode 100644 index 0000000000000..375978b9b7944 --- /dev/null +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; +import { CiStatsReporter } from '@kbn/dev-utils'; + +import { OptimizerUpdate$ } from './run_optimizer'; +import { OptimizerState } from './optimizer'; +import { pipeClosure } from './common'; + +export function reportOptimizerStats(reporter: CiStatsReporter, name: string) { + return pipeClosure((update$: OptimizerUpdate$) => { + let lastState: OptimizerState | undefined; + return update$.pipe( + materialize(), + mergeMap(async n => { + if (n.kind === 'N' && n.value?.state) { + lastState = n.value?.state; + } + + if (n.kind === 'C' && lastState) { + await reporter.metric('@kbn/optimizer build time', name, lastState.durSec); + } + + return n; + }), + dematerialize() + ); + }); +} diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 7411e2df1b613..cc3fa8c2720de 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 b921956642728..28cf36dedba3f 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,21 +94,21 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(703); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(500); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(502); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; }); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(517); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(576); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(687); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(689); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2506,9 +2506,9 @@ module.exports = require("path"); __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(584); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(684); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(685); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(686); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(687); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -2549,10 +2549,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_link_project_executables__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(19); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(499); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(583); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(502); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(580); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(585); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -4517,6 +4517,7 @@ exports.REPO_ROOT = repo_root_1.REPO_ROOT; var kbn_client_1 = __webpack_require__(449); exports.KbnClient = kbn_client_1.KbnClient; tslib_1.__exportStar(__webpack_require__(492), exports); +tslib_1.__exportStar(__webpack_require__(499), exports); /***/ }), @@ -40169,9 +40170,9 @@ module.exports = __webpack_require__(454); var utils = __webpack_require__(455); var bind = __webpack_require__(456); -var Axios = __webpack_require__(458); +var Axios = __webpack_require__(457); var mergeConfig = __webpack_require__(488); -var defaults = __webpack_require__(464); +var defaults = __webpack_require__(463); /** * Create an instance of Axios @@ -40206,7 +40207,7 @@ axios.create = function create(instanceConfig) { // Expose Cancel & CancelToken axios.Cancel = __webpack_require__(489); axios.CancelToken = __webpack_require__(490); -axios.isCancel = __webpack_require__(463); +axios.isCancel = __webpack_require__(462); // Expose all/spread axios.all = function all(promises) { @@ -40228,7 +40229,6 @@ module.exports.default = axios; var bind = __webpack_require__(456); -var isBuffer = __webpack_require__(457); /*global toString:true*/ @@ -40246,6 +40246,27 @@ function isArray(val) { return toString.call(val) === '[object Array]'; } +/** + * Determine if a value is undefined + * + * @param {Object} val The value to test + * @returns {boolean} True if the value is undefined, otherwise false + */ +function isUndefined(val) { + return typeof val === 'undefined'; +} + +/** + * Determine if a value is a Buffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Buffer, otherwise false + */ +function isBuffer(val) { + return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) + && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val); +} + /** * Determine if a value is an ArrayBuffer * @@ -40302,16 +40323,6 @@ function isNumber(val) { return typeof val === 'number'; } -/** - * Determine if a value is undefined - * - * @param {Object} val The value to test - * @returns {boolean} True if the value is undefined, otherwise false - */ -function isUndefined(val) { - return typeof val === 'undefined'; -} - /** * Determine if a value is an Object * @@ -40581,32 +40592,15 @@ module.exports = function bind(fn, thisArg) { /***/ }), /* 457 */ -/***/ (function(module, exports) { - -/*! - * Determine if an object is a Buffer - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -module.exports = function isBuffer (obj) { - return obj != null && obj.constructor != null && - typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj) -} - - -/***/ }), -/* 458 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var buildURL = __webpack_require__(459); -var InterceptorManager = __webpack_require__(460); -var dispatchRequest = __webpack_require__(461); +var buildURL = __webpack_require__(458); +var InterceptorManager = __webpack_require__(459); +var dispatchRequest = __webpack_require__(460); var mergeConfig = __webpack_require__(488); /** @@ -40638,7 +40632,15 @@ Axios.prototype.request = function request(config) { } config = mergeConfig(this.defaults, config); - config.method = config.method ? config.method.toLowerCase() : 'get'; + + // Set config.method + if (config.method) { + config.method = config.method.toLowerCase(); + } else if (this.defaults.method) { + config.method = this.defaults.method.toLowerCase(); + } else { + config.method = 'get'; + } // Hook up interceptors middleware var chain = [dispatchRequest, undefined]; @@ -40690,7 +40692,7 @@ module.exports = Axios; /***/ }), -/* 459 */ +/* 458 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40768,7 +40770,7 @@ module.exports = function buildURL(url, params, paramsSerializer) { /***/ }), -/* 460 */ +/* 459 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40827,18 +40829,16 @@ module.exports = InterceptorManager; /***/ }), -/* 461 */ +/* 460 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var transformData = __webpack_require__(462); -var isCancel = __webpack_require__(463); -var defaults = __webpack_require__(464); -var isAbsoluteURL = __webpack_require__(486); -var combineURLs = __webpack_require__(487); +var transformData = __webpack_require__(461); +var isCancel = __webpack_require__(462); +var defaults = __webpack_require__(463); /** * Throws a `Cancel` if cancellation has been requested. @@ -40858,11 +40858,6 @@ function throwIfCancellationRequested(config) { module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); - // Support baseURL config - if (config.baseURL && !isAbsoluteURL(config.url)) { - config.url = combineURLs(config.baseURL, config.url); - } - // Ensure headers exist config.headers = config.headers || {}; @@ -40877,7 +40872,7 @@ module.exports = function dispatchRequest(config) { config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, - config.headers || {} + config.headers ); utils.forEach( @@ -40920,7 +40915,7 @@ module.exports = function dispatchRequest(config) { /***/ }), -/* 462 */ +/* 461 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40947,7 +40942,7 @@ module.exports = function transformData(data, headers, fns) { /***/ }), -/* 463 */ +/* 462 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40959,14 +40954,14 @@ module.exports = function isCancel(value) { /***/ }), -/* 464 */ +/* 463 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var normalizeHeaderName = __webpack_require__(465); +var normalizeHeaderName = __webpack_require__(464); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -40980,13 +40975,12 @@ function setContentTypeIfUnset(headers, value) { function getDefaultAdapter() { var adapter; - // Only Node.JS has a process variable that is of [[Class]] process - if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { - // For node use HTTP adapter - adapter = __webpack_require__(466); - } else if (typeof XMLHttpRequest !== 'undefined') { + if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter - adapter = __webpack_require__(482); + adapter = __webpack_require__(465); + } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + // For node use HTTP adapter + adapter = __webpack_require__(475); } return adapter; } @@ -41064,7 +41058,7 @@ module.exports = defaults; /***/ }), -/* 465 */ +/* 464 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41083,295 +41077,200 @@ module.exports = function normalizeHeaderName(headers, normalizedName) { /***/ }), -/* 466 */ +/* 465 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var settle = __webpack_require__(467); -var buildURL = __webpack_require__(459); -var http = __webpack_require__(470); -var https = __webpack_require__(471); -var httpFollow = __webpack_require__(472).http; -var httpsFollow = __webpack_require__(472).https; -var url = __webpack_require__(452); -var zlib = __webpack_require__(480); -var pkg = __webpack_require__(481); -var createError = __webpack_require__(468); -var enhanceError = __webpack_require__(469); - -var isHttps = /https:?/; +var settle = __webpack_require__(466); +var buildURL = __webpack_require__(458); +var buildFullPath = __webpack_require__(469); +var parseHeaders = __webpack_require__(472); +var isURLSameOrigin = __webpack_require__(473); +var createError = __webpack_require__(467); -/*eslint consistent-return:0*/ -module.exports = function httpAdapter(config) { - return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { - var timer; - var resolve = function resolve(value) { - clearTimeout(timer); - resolvePromise(value); - }; - var reject = function reject(value) { - clearTimeout(timer); - rejectPromise(value); - }; - var data = config.data; - var headers = config.headers; +module.exports = function xhrAdapter(config) { + return new Promise(function dispatchXhrRequest(resolve, reject) { + var requestData = config.data; + var requestHeaders = config.headers; - // Set User-Agent (required by some servers) - // Only set header if it hasn't been set in config - // See https://github.com/axios/axios/issues/69 - if (!headers['User-Agent'] && !headers['user-agent']) { - headers['User-Agent'] = 'axios/' + pkg.version; + if (utils.isFormData(requestData)) { + delete requestHeaders['Content-Type']; // Let the browser set it } - if (data && !utils.isStream(data)) { - if (Buffer.isBuffer(data)) { - // Nothing to do... - } else if (utils.isArrayBuffer(data)) { - data = Buffer.from(new Uint8Array(data)); - } else if (utils.isString(data)) { - data = Buffer.from(data, 'utf-8'); - } else { - return reject(createError( - 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', - config - )); - } - - // Add Content-Length header if data exists - headers['Content-Length'] = data.length; - } + var request = new XMLHttpRequest(); // HTTP basic authentication - var auth = undefined; if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; - auth = username + ':' + password; + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } - // Parse url - var parsed = url.parse(config.url); - var protocol = parsed.protocol || 'http:'; + var fullPath = buildFullPath(config.baseURL, config.url); + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); - if (!auth && parsed.auth) { - var urlAuth = parsed.auth.split(':'); - var urlUsername = urlAuth[0] || ''; - var urlPassword = urlAuth[1] || ''; - auth = urlUsername + ':' + urlPassword; - } + // Set the request timeout in MS + request.timeout = config.timeout; - if (auth) { - delete headers.Authorization; - } + // Listen for ready state + request.onreadystatechange = function handleLoad() { + if (!request || request.readyState !== 4) { + return; + } - var isHttpsRequest = isHttps.test(protocol); - var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + // With one exception: request that using file: protocol, most browsers + // will return status as 0 even though it's a successful request + if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + return; + } - var options = { - path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), - method: config.method.toUpperCase(), - headers: headers, - agent: agent, - auth: auth - }; + // Prepare the response + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; + var response = { + data: responseData, + status: request.status, + statusText: request.statusText, + headers: responseHeaders, + config: config, + request: request + }; - if (config.socketPath) { - options.socketPath = config.socketPath; - } else { - options.hostname = parsed.hostname; - options.port = parsed.port; - } + settle(resolve, reject, response); - var proxy = config.proxy; - if (!proxy && proxy !== false) { - var proxyEnv = protocol.slice(0, -1) + '_proxy'; - var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]; - if (proxyUrl) { - var parsedProxyUrl = url.parse(proxyUrl); - var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY; - var shouldProxy = true; + // Clean up request + request = null; + }; - if (noProxyEnv) { - var noProxy = noProxyEnv.split(',').map(function trim(s) { - return s.trim(); - }); + // Handle browser request cancellation (as opposed to a manual cancellation) + request.onabort = function handleAbort() { + if (!request) { + return; + } - shouldProxy = !noProxy.some(function proxyMatch(proxyElement) { - if (!proxyElement) { - return false; - } - if (proxyElement === '*') { - return true; - } - if (proxyElement[0] === '.' && - parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement && - proxyElement.match(/\./g).length === parsed.hostname.match(/\./g).length) { - return true; - } + reject(createError('Request aborted', config, 'ECONNABORTED', request)); - return parsed.hostname === proxyElement; - }); - } + // Clean up request + request = null; + }; + // Handle low level network errors + request.onerror = function handleError() { + // Real errors are hidden from us by the browser + // onerror should only fire if it's a network error + reject(createError('Network Error', config, null, request)); - if (shouldProxy) { - proxy = { - host: parsedProxyUrl.hostname, - port: parsedProxyUrl.port - }; + // Clean up request + request = null; + }; - if (parsedProxyUrl.auth) { - var proxyUrlAuth = parsedProxyUrl.auth.split(':'); - proxy.auth = { - username: proxyUrlAuth[0], - password: proxyUrlAuth[1] - }; - } - } + // Handle timeout + request.ontimeout = function handleTimeout() { + var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + if (config.timeoutErrorMessage) { + timeoutErrorMessage = config.timeoutErrorMessage; } - } + reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', + request)); - if (proxy) { - options.hostname = proxy.host; - options.host = proxy.host; - options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : ''); - options.port = proxy.port; - options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path; + // Clean up request + request = null; + }; - // Basic proxy authorization - if (proxy.auth) { - var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64'); - options.headers['Proxy-Authorization'] = 'Basic ' + base64; - } - } + // Add xsrf header + // This is only done if running in a standard browser environment. + // Specifically not if we're in a web worker, or react-native. + if (utils.isStandardBrowserEnv()) { + var cookies = __webpack_require__(474); - var transport; - var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true); - if (config.transport) { - transport = config.transport; - } else if (config.maxRedirects === 0) { - transport = isHttpsProxy ? https : http; - } else { - if (config.maxRedirects) { - options.maxRedirects = config.maxRedirects; + // Add xsrf header + var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? + cookies.read(config.xsrfCookieName) : + undefined; + + if (xsrfValue) { + requestHeaders[config.xsrfHeaderName] = xsrfValue; } - transport = isHttpsProxy ? httpsFollow : httpFollow; } - if (config.maxContentLength && config.maxContentLength > -1) { - options.maxBodyLength = config.maxContentLength; + // Add headers to the request + if ('setRequestHeader' in request) { + utils.forEach(requestHeaders, function setRequestHeader(val, key) { + if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { + // Remove Content-Type if data is undefined + delete requestHeaders[key]; + } else { + // Otherwise add header to the request + request.setRequestHeader(key, val); + } + }); } - // Create the request - var req = transport.request(options, function handleResponse(res) { - if (req.aborted) return; - - // uncompress the response body transparently if required - var stream = res; - switch (res.headers['content-encoding']) { - /*eslint default-case:0*/ - case 'gzip': - case 'compress': - case 'deflate': - // add the unzipper to the body stream processing pipeline - stream = (res.statusCode === 204) ? stream : stream.pipe(zlib.createUnzip()); - - // remove the content-encoding in order to not confuse downstream operations - delete res.headers['content-encoding']; - break; - } - - // return the last request in case of redirects - var lastRequest = res.req || req; - - var response = { - status: res.statusCode, - statusText: res.statusMessage, - headers: res.headers, - config: config, - request: lastRequest - }; - - if (config.responseType === 'stream') { - response.data = stream; - settle(resolve, reject, response); - } else { - var responseBuffer = []; - stream.on('data', function handleStreamData(chunk) { - responseBuffer.push(chunk); - - // make sure the content length is not over the maxContentLength if specified - if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { - stream.destroy(); - reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', - config, null, lastRequest)); - } - }); - - stream.on('error', function handleStreamError(err) { - if (req.aborted) return; - reject(enhanceError(err, config, null, lastRequest)); - }); - - stream.on('end', function handleStreamEnd() { - var responseData = Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - responseData = responseData.toString(config.responseEncoding); - } + // Add withCredentials to request if needed + if (!utils.isUndefined(config.withCredentials)) { + request.withCredentials = !!config.withCredentials; + } - response.data = responseData; - settle(resolve, reject, response); - }); + // Add responseType to request if needed + if (config.responseType) { + try { + request.responseType = config.responseType; + } catch (e) { + // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. + // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. + if (config.responseType !== 'json') { + throw e; + } } - }); + } - // Handle errors - req.on('error', function handleRequestError(err) { - if (req.aborted) return; - reject(enhanceError(err, config, null, req)); - }); + // Handle progress if needed + if (typeof config.onDownloadProgress === 'function') { + request.addEventListener('progress', config.onDownloadProgress); + } - // Handle request timeout - if (config.timeout) { - timer = setTimeout(function handleRequestTimeout() { - req.abort(); - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); - }, config.timeout); + // Not all browsers support upload events + if (typeof config.onUploadProgress === 'function' && request.upload) { + request.upload.addEventListener('progress', config.onUploadProgress); } if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { - if (req.aborted) return; + if (!request) { + return; + } - req.abort(); + request.abort(); reject(cancel); + // Clean up request + request = null; }); } - // Send the request - if (utils.isStream(data)) { - data.on('error', function handleStreamError(err) { - reject(enhanceError(err, config, null, req)); - }).pipe(req); - } else { - req.end(data); + if (requestData === undefined) { + requestData = null; } + + // Send the request + request.send(requestData); }); }; /***/ }), -/* 467 */ +/* 466 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var createError = __webpack_require__(468); +var createError = __webpack_require__(467); /** * Resolve or reject a Promise based on response status. @@ -41397,13 +41296,13 @@ module.exports = function settle(resolve, reject, response) { /***/ }), -/* 468 */ +/* 467 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var enhanceError = __webpack_require__(469); +var enhanceError = __webpack_require__(468); /** * Create an Error with the specified message, config, error code, request and response. @@ -41422,7 +41321,7 @@ module.exports = function createError(message, config, code, request, response) /***/ }), -/* 469 */ +/* 468 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41470,28 +41369,578 @@ 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, __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); +}; + + +/***/ }), +/* 471 */ +/***/ (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; +}; + + +/***/ }), +/* 472 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(455); + +// 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' +]; + +/** + * 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; + + 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)); + + 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; + } + } + }); + + return parsed; +}; + + +/***/ }), +/* 473 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(455); + +module.exports = ( + utils.isStandardBrowserEnv() ? + + // 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; + + /** + * Parse a URL to discover it's components + * + * @param {String} url The URL to be parsed + * @returns {Object} + */ + function resolveURL(url) { + var href = url; + + if (msie) { + // IE needs attribute set twice to normalize properties + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // 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 + }; + } + + originURL = resolveURL(window.location.href); + + /** + * 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); + }; + })() : + + // Non standard browser envs (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return function isURLSameOrigin() { + return true; + }; + })() +); + + +/***/ }), +/* 474 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(455); + +module.exports = ( + utils.isStandardBrowserEnv() ? + + // 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)); + + if (utils.isNumber(expires)) { + cookie.push('expires=' + new Date(expires).toGMTString()); + } + + if (utils.isString(path)) { + cookie.push('path=' + path); + } + + if (utils.isString(domain)) { + cookie.push('domain=' + domain); + } + + if (secure === true) { + cookie.push('secure'); + } + + document.cookie = cookie.join('; '); + }, + + 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() {} + }; + })() +); + + +/***/ }), +/* 475 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +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); + +var isHttps = /https:?/; + +/*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; + + // 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 (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; + } + + // HTTP basic authentication + var auth = undefined; + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password || ''; + auth = username + ':' + password; + } + + // 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; + } + + if (auth) { + delete headers.Authorization; + } + + var isHttpsRequest = isHttps.test(protocol); + var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + + 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 (config.socketPath) { + options.socketPath = config.socketPath; + } else { + options.hostname = parsed.hostname; + options.port = parsed.port; + } + + 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; + + if (noProxyEnv) { + var noProxy = noProxyEnv.split(',').map(function trim(s) { + return s.trim(); + }); + + 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; + } + + return parsed.hostname === proxyElement; + }); + } + + + if (shouldProxy) { + proxy = { + host: parsedProxyUrl.hostname, + port: parsedProxyUrl.port + }; + + if (parsedProxyUrl.auth) { + var proxyUrlAuth = parsedProxyUrl.auth.split(':'); + proxy.auth = { + username: proxyUrlAuth[0], + password: proxyUrlAuth[1] + }; + } + } + } + } + + 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; + + // 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; + } + } + + 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; + } + + if (config.maxContentLength && config.maxContentLength > -1) { + options.maxBodyLength = config.maxContentLength; + } + + // 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); + } + + response.data = responseData; + settle(resolve, reject, response); + }); + } + }); + + // Handle errors + req.on('error', function handleRequestError(err) { + if (req.aborted) return; + reject(enhanceError(err, config, null, req)); + }); + + // 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)); + }); + } + + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + if (req.aborted) return; + + req.abort(); + reject(cancel); + }); + } + + // 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); + } + }); +}; + + +/***/ }), +/* 476 */ /***/ (function(module, exports) { module.exports = require("http"); /***/ }), -/* 471 */ +/* 477 */ /***/ (function(module, exports) { module.exports = require("https"); /***/ }), -/* 472 */ +/* 478 */ /***/ (function(module, exports, __webpack_require__) { var url = __webpack_require__(452); -var http = __webpack_require__(470); -var https = __webpack_require__(471); +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__(473)("follow-redirects"); +var debug = __webpack_require__(479)("follow-redirects"); // RFC7231§4.2.1: Of the request methods defined by this specification, // the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. @@ -41811,7 +42260,7 @@ module.exports.wrap = wrap; /***/ }), -/* 473 */ +/* 479 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -41820,14 +42269,14 @@ module.exports.wrap = wrap; */ if (typeof process === 'undefined' || process.type === 'renderer') { - module.exports = __webpack_require__(474); + module.exports = __webpack_require__(480); } else { - module.exports = __webpack_require__(477); + module.exports = __webpack_require__(483); } /***/ }), -/* 474 */ +/* 480 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -41836,7 +42285,7 @@ if (typeof process === 'undefined' || process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(475); +exports = module.exports = __webpack_require__(481); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -42028,7 +42477,7 @@ function localstorage() { /***/ }), -/* 475 */ +/* 481 */ /***/ (function(module, exports, __webpack_require__) { @@ -42044,7 +42493,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(476); +exports.humanize = __webpack_require__(482); /** * Active `debug` instances. @@ -42259,7 +42708,7 @@ function coerce(val) { /***/ }), -/* 476 */ +/* 482 */ /***/ (function(module, exports) { /** @@ -42417,14 +42866,14 @@ function plural(ms, n, name) { /***/ }), -/* 477 */ +/* 483 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(478); +var tty = __webpack_require__(484); var util = __webpack_require__(29); /** @@ -42433,7 +42882,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(475); +exports = module.exports = __webpack_require__(481); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -42448,7 +42897,7 @@ exports.useColors = useColors; exports.colors = [ 6, 2, 3, 4, 5, 1 ]; try { - var supportsColor = __webpack_require__(479); + 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, @@ -42587,600 +43036,182 @@ function load() { /** * 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 keys = Object.keys(exports.inspectOpts); - for (var i = 0; i < keys.length; i++) { - debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; - } -} - -/** - * Enable namespaces listed in `process.env.DEBUG` initially. - */ - -exports.enable(load()); - - -/***/ }), -/* 478 */ -/***/ (function(module, exports) { - -module.exports = require("tty"); - -/***/ }), -/* 479 */ -/***/ (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 - }; -} - -function supportsColor(stream) { - if (forceColor === false) { - return 0; - } - - 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) -}; - - -/***/ }), -/* 480 */ -/***/ (function(module, exports) { - -module.exports = require("zlib"); - -/***/ }), -/* 481 */ -/***/ (function(module) { - -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\"}]}"); - -/***/ }), -/* 482 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -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); - -module.exports = function xhrAdapter(config) { - return new Promise(function dispatchXhrRequest(resolve, reject) { - var requestData = config.data; - var requestHeaders = config.headers; - - if (utils.isFormData(requestData)) { - delete requestHeaders['Content-Type']; // Let the browser set it - } - - var request = new XMLHttpRequest(); - - // HTTP basic authentication - if (config.auth) { - var username = config.auth.username || ''; - var password = config.auth.password || ''; - requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); - } - - request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true); - - // Set the request timeout in MS - request.timeout = config.timeout; - - // Listen for ready state - request.onreadystatechange = function handleLoad() { - if (!request || request.readyState !== 4) { - return; - } - - // 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; - } - - // 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; - }; - - // 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)); - - // Clean up request - request = null; - }; - - // Handle timeout - request.ontimeout = function handleTimeout() { - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', - request)); - - // Clean up request - request = null; - }; - - // 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); - - // Add xsrf header - var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ? - cookies.read(config.xsrfCookieName) : - undefined; - - if (xsrfValue) { - requestHeaders[config.xsrfHeaderName] = xsrfValue; - } - } - - // 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); - } - }); - } - - // Add withCredentials to request if needed - if (config.withCredentials) { - request.withCredentials = true; - } - - // 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 progress if needed - if (typeof config.onDownloadProgress === 'function') { - request.addEventListener('progress', config.onDownloadProgress); - } - - // 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 (!request) { - return; - } - - request.abort(); - reject(cancel); - // Clean up request - request = null; - }); - } - - if (requestData === undefined) { - requestData = null; - } - - // Send the request - request.send(requestData); - }); -}; - - -/***/ }), -/* 483 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var utils = __webpack_require__(455); - -// 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' -]; - -/** - * 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 + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. */ -module.exports = function parseHeaders(headers) { - var parsed = {}; - var key; - var val; - var i; - if (!headers) { return parsed; } +function init (debug) { + debug.inspectOpts = {}; - 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)); + var keys = Object.keys(exports.inspectOpts); + for (var i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + } +} - 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; - } - } - }); +/** + * Enable namespaces listed in `process.env.DEBUG` initially. + */ - return parsed; -}; +exports.enable(load()); /***/ }), /* 484 */ +/***/ (function(module, exports) { + +module.exports = require("tty"); + +/***/ }), +/* 485 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const os = __webpack_require__(11); +const hasFlag = __webpack_require__(12); -var utils = __webpack_require__(455); - -module.exports = ( - utils.isStandardBrowserEnv() ? +const env = process.env; - // 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; +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; +} - /** - * Parse a URL to discover it's components - * - * @param {String} url The URL to be parsed - * @returns {Object} - */ - function resolveURL(url) { - var href = url; +function translateLevel(level) { + if (level === 0) { + return false; + } - if (msie) { - // IE needs attribute set twice to normalize properties - urlParsingNode.setAttribute('href', href); - href = urlParsingNode.href; - } + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3 + }; +} - urlParsingNode.setAttribute('href', href); +function supportsColor(stream) { + if (forceColor === false) { + return 0; + } - // 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 - }; - } + if (hasFlag('color=16m') || + hasFlag('color=full') || + hasFlag('color=truecolor')) { + return 3; + } - originURL = resolveURL(window.location.href); + if (hasFlag('color=256')) { + return 2; + } - /** - * 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); - }; - })() : + if (stream && !stream.isTTY && forceColor !== true) { + return 0; + } - // Non standard browser envs (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return function isURLSameOrigin() { - return true; - }; - })() -); + 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; + } -/***/ }), -/* 485 */ -/***/ (function(module, exports, __webpack_require__) { + return 1; + } -"use strict"; + if ('CI' in env) { + if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + return min; + } -var utils = __webpack_require__(455); + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } -module.exports = ( - utils.isStandardBrowserEnv() ? + if (env.COLORTERM === 'truecolor') { + return 3; + } - // 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)); + if ('TERM_PROGRAM' in env) { + const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); - if (utils.isNumber(expires)) { - cookie.push('expires=' + new Date(expires).toGMTString()); - } + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + return version >= 3 ? 3 : 2; + case 'Apple_Terminal': + return 2; + // No default + } + } - if (utils.isString(path)) { - cookie.push('path=' + path); - } + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } - if (utils.isString(domain)) { - cookie.push('domain=' + domain); - } + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } - if (secure === true) { - cookie.push('secure'); - } + if ('COLORTERM' in env) { + return 1; + } - document.cookie = cookie.join('; '); - }, + if (env.TERM === 'dumb') { + return min; + } - read: function read(name) { - var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); - return (match ? decodeURIComponent(match[3]) : null); - }, + return min; +} - remove: function remove(name) { - this.write(name, '', Date.now() - 86400000); - } - }; - })() : +function getSupportLevel(stream) { + const level = supportsColor(stream); + return translateLevel(level); +} - // 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() {} - }; - })() -); +module.exports = { + supportsColor: getSupportLevel, + stdout: getSupportLevel(process.stdout), + stderr: getSupportLevel(process.stderr) +}; /***/ }), /* 486 */ -/***/ (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); -}; +/***/ (function(module, exports) { +module.exports = require("zlib"); /***/ }), /* 487 */ -/***/ (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; -}; +/***/ (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 */ @@ -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') { @@ -43591,92 +43644,221 @@ exports.KbnClientVersion = KbnClientVersion; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(451); -class KbnClientSavedObjects { - constructor(log, requester) { - this.log = log; - this.requester = requester; - } - /** - * Run the saved objects migration - */ - async migrate() { - this.log.debug('Migrating saved objects'); - return await this.requester.request({ - description: 'migrate saved objects', - path: kbn_client_requester_1.uriencode `/internal/saved_objects/_migrate`, - method: 'POST', - body: {}, - }); - } - /** - * Get an object - */ - async get(options) { - this.log.debug('Gettings saved object: %j', options); - return await this.requester.request({ - description: 'get saved object', - path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, - method: 'GET', - }); - } - /** - * Create a saved object - */ - async create(options) { - this.log.debug('Creating saved object: %j', options); - return await this.requester.request({ - description: 'update saved object', - path: options.id - ? kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}` - : kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}`, - query: { - overwrite: options.overwrite, - }, - method: 'POST', - body: { - attributes: options.attributes, - migrationVersion: options.migrationVersion, - references: options.references, - }, - }); - } - /** - * Update a saved object - */ - async update(options) { - this.log.debug('Updating saved object: %j', options); - return await this.requester.request({ - description: 'update saved object', - path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, - query: { - overwrite: options.overwrite, - }, - method: 'PUT', - body: { - attributes: options.attributes, - migrationVersion: options.migrationVersion, - references: options.references, - }, - }); - } - /** - * Delete an object - */ - async delete(options) { - this.log.debug('Deleting saved object %s/%s', options); - return await this.requester.request({ - description: 'delete saved object', - path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, - method: 'DELETE', - }); - } -} -exports.KbnClientSavedObjects = KbnClientSavedObjects; +const kbn_client_requester_1 = __webpack_require__(451); +class KbnClientSavedObjects { + constructor(log, requester) { + this.log = log; + this.requester = requester; + } + /** + * Run the saved objects migration + */ + async migrate() { + this.log.debug('Migrating saved objects'); + return await this.requester.request({ + description: 'migrate saved objects', + path: kbn_client_requester_1.uriencode `/internal/saved_objects/_migrate`, + method: 'POST', + body: {}, + }); + } + /** + * Get an object + */ + async get(options) { + this.log.debug('Gettings saved object: %j', options); + return await this.requester.request({ + description: 'get saved object', + path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, + method: 'GET', + }); + } + /** + * Create a saved object + */ + async create(options) { + this.log.debug('Creating saved object: %j', options); + return await this.requester.request({ + description: 'update saved object', + path: options.id + ? kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}` + : kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}`, + query: { + overwrite: options.overwrite, + }, + method: 'POST', + body: { + attributes: options.attributes, + migrationVersion: options.migrationVersion, + references: options.references, + }, + }); + } + /** + * Update a saved object + */ + async update(options) { + this.log.debug('Updating saved object: %j', options); + return await this.requester.request({ + description: 'update saved object', + path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, + query: { + overwrite: options.overwrite, + }, + method: 'PUT', + body: { + attributes: options.attributes, + migrationVersion: options.migrationVersion, + references: options.references, + }, + }); + } + /** + * Delete an object + */ + async delete(options) { + this.log.debug('Deleting saved object %s/%s', options); + return await this.requester.request({ + description: 'delete saved object', + path: kbn_client_requester_1.uriencode `/api/saved_objects/${options.type}/${options.id}`, + method: 'DELETE', + }); + } +} +exports.KbnClientSavedObjects = KbnClientSavedObjects; + + +/***/ }), +/* 498 */ +/***/ (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 kbn_client_requester_1 = __webpack_require__(451); +class KbnClientUiSettings { + constructor(log, requester, defaults) { + this.log = log; + this.requester = requester; + this.defaults = defaults; + } + async get(setting) { + var _a; + const all = await this.getAll(); + const value = (_a = all[setting]) === null || _a === void 0 ? void 0 : _a.userValue; + this.log.verbose('uiSettings.value: %j', value); + return value; + } + /** + * Gets defaultIndex from the config doc. + */ + async getDefaultIndex() { + return await this.get('defaultIndex'); + } + /** + * Unset a uiSetting + */ + async unset(setting) { + return await this.requester.request({ + path: kbn_client_requester_1.uriencode `/api/kibana/settings/${setting}`, + method: 'DELETE', + }); + } + /** + * Replace all uiSettings with the `doc` values, `doc` is merged + * with some defaults + */ + async replace(doc, { retries = 5 } = {}) { + this.log.debug('replacing kibana config doc: %j', doc); + const changes = { + ...this.defaults, + ...doc, + }; + for (const [name, { isOverridden }] of Object.entries(await this.getAll())) { + if (!isOverridden && !changes.hasOwnProperty(name)) { + changes[name] = null; + } + } + await this.requester.request({ + method: 'POST', + path: '/api/kibana/settings', + body: { changes }, + retries, + }); + } + /** + * Add fields to the config doc (like setting timezone and defaultIndex) + */ + async update(updates) { + this.log.debug('applying update to kibana config: %j', updates); + await this.requester.request({ + path: '/api/kibana/settings', + method: 'POST', + body: { + changes: updates, + }, + }); + } + async getAll() { + const resp = await this.requester.request({ + path: '/api/kibana/settings', + method: 'GET', + }); + return resp.settings; + } +} +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); /***/ }), -/* 498 */ +/* 500 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43700,83 +43882,113 @@ exports.KbnClientSavedObjects = KbnClientSavedObjects; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(451); -class KbnClientUiSettings { - constructor(log, requester, defaults) { - this.log = log; - this.requester = requester; - this.defaults = defaults; +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; } - async get(setting) { - var _a; - const all = await this.getAll(); - const value = (_a = all[setting]) === null || _a === void 0 ? void 0 : _a.userValue; - this.log.verbose('uiSettings.value: %j', value); - return value; + let config; + try { + config = JSON.parse(configJson); } - /** - * Gets defaultIndex from the config doc. - */ - async getDefaultIndex() { - return await this.get('defaultIndex'); + catch (_) { + // handled below } - /** - * Unset a uiSetting - */ - async unset(setting) { - return await this.requester.request({ - path: kbn_client_requester_1.uriencode `/api/kibana/settings/${setting}`, - method: 'DELETE', - }); + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config); } - /** - * Replace all uiSettings with the `doc` values, `doc` is merged - * with some defaults - */ - async replace(doc, { retries = 5 } = {}) { - this.log.debug('replacing kibana config doc: %j', doc); - const changes = { - ...this.defaults, - ...doc, - }; - for (const [name, { isOverridden }] of Object.entries(await this.getAll())) { - if (!isOverridden && !changes.hasOwnProperty(name)) { - changes[name] = null; - } - } - await this.requester.request({ - method: 'POST', - path: '/api/kibana/settings', - body: { changes }, - retries, - }); + 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; } - /** - * Add fields to the config doc (like setting timezone and defaultIndex) - */ - async update(updates) { - this.log.debug('applying update to kibana config: %j', updates); - await this.requester.request({ - path: '/api/kibana/settings', - method: 'POST', - body: { - changes: updates, - }, - }); + 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; } - async getAll() { - const resp = await this.requester.request({ - path: '/api/kibana/settings', - method: 'GET', - }); - return resp.settings; + 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; } -exports.KbnClientUiSettings = KbnClientUiSettings; +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; /***/ }), -/* 499 */ +/* 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__(939); +/* 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__(924); -const junk = __webpack_require__(936); -const CpyError = __webpack_require__(937); +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__(917); -const gitignore = __webpack_require__(920); +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__(888); -var reader_stream_1 = __webpack_require__(912); -var reader_sync_1 = __webpack_require__(913); -var arrayUtils = __webpack_require__(915); -var streamUtils = __webpack_require__(916); +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__(840); -var extend = __webpack_require__(848); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(842); +var extend = __webpack_require__(850); /** * Local dependencies */ -var compilers = __webpack_require__(851); -var parsers = __webpack_require__(884); -var cache = __webpack_require__(885); -var utils = __webpack_require__(886); +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__(742); -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__(743); -var parsers = __webpack_require__(760); -var Braces = __webpack_require__(770); -var utils = __webpack_require__(744); +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__(740); +var extend = __webpack_require__(742); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84199,13 +84414,13 @@ module.exports = toRegex; /***/ }), -/* 740 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(741); +var isObject = __webpack_require__(743); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84239,7 +84454,7 @@ function hasOwn(obj, key) { /***/ }), -/* 741 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84259,7 +84474,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 742 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84309,13 +84524,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 743 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(744); +var utils = __webpack_require__(746); module.exports = function(braces, options) { braces.compiler @@ -84598,25 +84813,25 @@ function hasQueue(node) { /***/ }), -/* 744 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(745); +var splitString = __webpack_require__(747); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(737); -utils.flatten = __webpack_require__(751); -utils.isObject = __webpack_require__(749); -utils.fillRange = __webpack_require__(752); -utils.repeat = __webpack_require__(759); -utils.unique = __webpack_require__(742); +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, { @@ -84948,7 +85163,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 745 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84961,7 +85176,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(746); +var extend = __webpack_require__(748); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85126,14 +85341,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 746 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(747); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(749); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85193,7 +85408,7 @@ function isEnum(obj, key) { /***/ }), -/* 747 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85206,7 +85421,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85214,7 +85429,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 748 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85227,7 +85442,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(751); function isObjectObject(o) { return isObject(o) === true @@ -85258,7 +85473,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 749 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85277,7 +85492,7 @@ module.exports = function isObject(val) { /***/ }), -/* 750 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85324,7 +85539,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 751 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85353,7 +85568,7 @@ function flat(arr, res) { /***/ }), -/* 752 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85367,10 +85582,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(753); -var extend = __webpack_require__(755); -var repeat = __webpack_require__(757); -var toRegex = __webpack_require__(758); +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. @@ -85568,7 +85783,7 @@ module.exports = fillRange; /***/ }), -/* 753 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85581,7 +85796,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(756); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85597,10 +85812,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 754 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85719,13 +85934,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 755 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(756); +var isObject = __webpack_require__(758); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -85759,7 +85974,7 @@ function hasOwn(obj, key) { /***/ }), -/* 756 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85779,7 +85994,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 757 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85856,7 +86071,7 @@ function repeat(str, num) { /***/ }), -/* 758 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85869,8 +86084,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(757); -var isNumber = __webpack_require__(753); +var repeat = __webpack_require__(759); +var isNumber = __webpack_require__(755); var cache = {}; function toRegexRange(min, max, options) { @@ -86157,7 +86372,7 @@ module.exports = toRegexRange; /***/ }), -/* 759 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86182,14 +86397,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 760 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(761); -var utils = __webpack_require__(744); +var Node = __webpack_require__(763); +var utils = __webpack_require__(746); /** * Braces parsers @@ -86549,15 +86764,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 761 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(749); -var define = __webpack_require__(762); -var utils = __webpack_require__(769); +var isObject = __webpack_require__(751); +var define = __webpack_require__(764); +var utils = __webpack_require__(771); var ownNames; /** @@ -87048,7 +87263,7 @@ exports = module.exports = Node; /***/ }), -/* 762 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87061,7 +87276,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(763); +var isDescriptor = __webpack_require__(765); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87086,7 +87301,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 763 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87099,9 +87314,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(764); -var isAccessor = __webpack_require__(765); -var isData = __webpack_require__(767); +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') { @@ -87115,7 +87330,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 764 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87250,7 +87465,7 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87263,7 +87478,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(766); +var typeOf = __webpack_require__(768); // accessor descriptor properties var accessor = { @@ -87326,7 +87541,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 766 */ +/* 768 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87461,7 +87676,7 @@ function isBuffer(val) { /***/ }), -/* 767 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87474,7 +87689,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(768); +var typeOf = __webpack_require__(770); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87517,7 +87732,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 768 */ +/* 770 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87652,13 +87867,13 @@ function isBuffer(val) { /***/ }), -/* 769 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(756); var utils = module.exports; /** @@ -88678,17 +88893,17 @@ function assert(val, message) { /***/ }), -/* 770 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(771); -var compilers = __webpack_require__(743); -var parsers = __webpack_require__(760); -var utils = __webpack_require__(744); +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 @@ -88789,17 +89004,17 @@ module.exports = Braces; /***/ }), -/* 771 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(772); -var define = __webpack_require__(798); -var Compiler = __webpack_require__(808); -var Parser = __webpack_require__(837); -var utils = __webpack_require__(817); +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 = {}; @@ -88970,20 +89185,20 @@ module.exports.Parser = Parser; /***/ }), -/* 772 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(773); -var CacheBase = __webpack_require__(774); -var Emitter = __webpack_require__(775); -var isObject = __webpack_require__(749); -var merge = __webpack_require__(792); -var pascal = __webpack_require__(795); -var cu = __webpack_require__(796); +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. @@ -89412,7 +89627,7 @@ module.exports.namespace = namespace; /***/ }), -/* 773 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89425,7 +89640,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(763); +var isDescriptor = __webpack_require__(765); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89450,21 +89665,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 774 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(749); -var Emitter = __webpack_require__(775); -var visit = __webpack_require__(776); -var toPath = __webpack_require__(779); -var union = __webpack_require__(780); -var del = __webpack_require__(784); -var get = __webpack_require__(782); -var has = __webpack_require__(789); -var set = __webpack_require__(783); +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); /** * Create a `Cache` constructor that when instantiated will @@ -89718,7 +89933,7 @@ module.exports.namespace = namespace; /***/ }), -/* 775 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { @@ -89887,7 +90102,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 776 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89900,8 +90115,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(777); -var mapVisit = __webpack_require__(778); +var visit = __webpack_require__(779); +var mapVisit = __webpack_require__(780); module.exports = function(collection, method, val) { var result; @@ -89924,7 +90139,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 777 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89937,7 +90152,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(751); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -89964,14 +90179,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 778 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(777); +var visit = __webpack_require__(779); /** * Map `visit` over an array of objects. @@ -90008,7 +90223,7 @@ function isObject(val) { /***/ }), -/* 779 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90021,7 +90236,7 @@ function isObject(val) { -var typeOf = __webpack_require__(754); +var typeOf = __webpack_require__(756); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90048,16 +90263,16 @@ function filter(arr) { /***/ }), -/* 780 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(741); -var union = __webpack_require__(781); -var get = __webpack_require__(782); -var set = __webpack_require__(783); +var isObject = __webpack_require__(743); +var union = __webpack_require__(783); +var get = __webpack_require__(784); +var set = __webpack_require__(785); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90085,7 +90300,7 @@ function arrayify(val) { /***/ }), -/* 781 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90121,7 +90336,7 @@ module.exports = function union(init) { /***/ }), -/* 782 */ +/* 784 */ /***/ (function(module, exports) { /*! @@ -90177,7 +90392,7 @@ function toString(val) { /***/ }), -/* 783 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90190,10 +90405,10 @@ function toString(val) { -var split = __webpack_require__(745); -var extend = __webpack_require__(740); -var isPlainObject = __webpack_require__(748); -var isObject = __webpack_require__(741); +var split = __webpack_require__(747); +var extend = __webpack_require__(742); +var isPlainObject = __webpack_require__(750); +var isObject = __webpack_require__(743); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90239,7 +90454,7 @@ function isValidKey(key) { /***/ }), -/* 784 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90252,8 +90467,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(749); -var has = __webpack_require__(785); +var isObject = __webpack_require__(751); +var has = __webpack_require__(787); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90278,7 +90493,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 785 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90291,9 +90506,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(786); -var hasValues = __webpack_require__(788); -var get = __webpack_require__(782); +var isObject = __webpack_require__(788); +var hasValues = __webpack_require__(790); +var get = __webpack_require__(784); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90304,7 +90519,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 786 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90317,7 +90532,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(787); +var isArray = __webpack_require__(789); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90325,7 +90540,7 @@ module.exports = function isObject(val) { /***/ }), -/* 787 */ +/* 789 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90336,7 +90551,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 788 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90379,7 +90594,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 789 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90392,9 +90607,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(749); -var hasValues = __webpack_require__(790); -var get = __webpack_require__(782); +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); @@ -90402,7 +90617,7 @@ module.exports = function(val, prop) { /***/ }), -/* 790 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90415,8 +90630,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(791); -var isNumber = __webpack_require__(753); +var typeOf = __webpack_require__(793); +var isNumber = __webpack_require__(755); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90469,10 +90684,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 791 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -90594,14 +90809,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 792 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(793); -var forIn = __webpack_require__(794); +var isExtendable = __webpack_require__(795); +var forIn = __webpack_require__(796); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90665,7 +90880,7 @@ module.exports = mixinDeep; /***/ }), -/* 793 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90678,7 +90893,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90686,7 +90901,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 794 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90709,7 +90924,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 795 */ +/* 797 */ /***/ (function(module, exports) { /*! @@ -90736,14 +90951,14 @@ module.exports = pascalcase; /***/ }), -/* 796 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(797); +var utils = __webpack_require__(799); /** * Expose class utils @@ -91108,7 +91323,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 797 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91122,10 +91337,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(781); -utils.define = __webpack_require__(798); -utils.isObj = __webpack_require__(749); -utils.staticExtend = __webpack_require__(805); +utils.union = __webpack_require__(783); +utils.define = __webpack_require__(800); +utils.isObj = __webpack_require__(751); +utils.staticExtend = __webpack_require__(807); /** @@ -91136,7 +91351,7 @@ module.exports = utils; /***/ }), -/* 798 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91149,7 +91364,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(799); +var isDescriptor = __webpack_require__(801); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -91174,7 +91389,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 799 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91187,9 +91402,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(800); -var isAccessor = __webpack_require__(801); -var isData = __webpack_require__(803); +var typeOf = __webpack_require__(802); +var isAccessor = __webpack_require__(803); +var isData = __webpack_require__(805); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -91203,7 +91418,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 800 */ +/* 802 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -91356,7 +91571,7 @@ function isBuffer(val) { /***/ }), -/* 801 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91369,7 +91584,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(802); +var typeOf = __webpack_require__(804); // accessor descriptor properties var accessor = { @@ -91432,10 +91647,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 802 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -91554,7 +91769,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 803 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91567,7 +91782,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(804); +var typeOf = __webpack_require__(806); // data descriptor properties var data = { @@ -91616,10 +91831,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 804 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -91738,7 +91953,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 805 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91751,8 +91966,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(806); -var define = __webpack_require__(798); +var copy = __webpack_require__(808); +var define = __webpack_require__(800); var util = __webpack_require__(29); /** @@ -91835,15 +92050,15 @@ module.exports = extend; /***/ }), -/* 806 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(754); -var copyDescriptor = __webpack_require__(807); -var define = __webpack_require__(798); +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. @@ -92016,7 +92231,7 @@ module.exports.has = has; /***/ }), -/* 807 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92104,16 +92319,16 @@ function isObject(val) { /***/ }), -/* 808 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(809); -var define = __webpack_require__(798); -var debug = __webpack_require__(811)('snapdragon:compiler'); -var utils = __webpack_require__(817); +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`. @@ -92267,7 +92482,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(836); + var sourcemaps = __webpack_require__(838); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -92288,7 +92503,7 @@ module.exports = Compiler; /***/ }), -/* 809 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92301,7 +92516,7 @@ module.exports = Compiler; -var utils = __webpack_require__(810); +var utils = __webpack_require__(812); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -92416,7 +92631,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 810 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92430,8 +92645,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(798); -utils.isObject = __webpack_require__(749); +utils.define = __webpack_require__(800); +utils.isObject = __webpack_require__(751); utils.isString = function(val) { @@ -92446,7 +92661,7 @@ module.exports = utils; /***/ }), -/* 811 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92455,14 +92670,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(812); + module.exports = __webpack_require__(814); } else { - module.exports = __webpack_require__(815); + module.exports = __webpack_require__(817); } /***/ }), -/* 812 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92471,7 +92686,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(813); +exports = module.exports = __webpack_require__(815); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92653,7 +92868,7 @@ function localstorage() { /***/ }), -/* 813 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { @@ -92669,7 +92884,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(814); +exports.humanize = __webpack_require__(816); /** * The currently active debug mode names, and names to skip. @@ -92861,7 +93076,7 @@ function coerce(val) { /***/ }), -/* 814 */ +/* 816 */ /***/ (function(module, exports) { /** @@ -93019,14 +93234,14 @@ function plural(ms, n, name) { /***/ }), -/* 815 */ +/* 817 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(478); +var tty = __webpack_require__(484); var util = __webpack_require__(29); /** @@ -93035,7 +93250,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(813); +exports = module.exports = __webpack_require__(815); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -93214,7 +93429,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(816); + var net = __webpack_require__(818); stream = new net.Socket({ fd: fd, readable: false, @@ -93273,13 +93488,13 @@ exports.enable(load()); /***/ }), -/* 816 */ +/* 818 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 817 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93289,9 +93504,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(740); -exports.SourceMap = __webpack_require__(818); -exports.sourceMapResolve = __webpack_require__(829); +exports.extend = __webpack_require__(742); +exports.SourceMap = __webpack_require__(820); +exports.sourceMapResolve = __webpack_require__(831); /** * Convert backslash in the given string to forward slashes @@ -93334,7 +93549,7 @@ exports.last = function(arr, n) { /***/ }), -/* 818 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -93342,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__(819).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(825).SourceMapConsumer; -exports.SourceNode = __webpack_require__(828).SourceNode; +exports.SourceMapGenerator = __webpack_require__(821).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(827).SourceMapConsumer; +exports.SourceNode = __webpack_require__(830).SourceNode; /***/ }), -/* 819 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93358,10 +93573,10 @@ exports.SourceNode = __webpack_require__(828).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(820); -var util = __webpack_require__(822); -var ArraySet = __webpack_require__(823).ArraySet; -var MappingList = __webpack_require__(824).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 @@ -93770,7 +93985,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 820 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93810,7 +94025,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(821); +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, @@ -93916,7 +94131,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 821 */ +/* 823 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93989,7 +94204,7 @@ exports.decode = function (charCode) { /***/ }), -/* 822 */ +/* 824 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94412,7 +94627,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 823 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94422,7 +94637,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(822); +var util = __webpack_require__(824); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -94539,7 +94754,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 824 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94549,7 +94764,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(822); +var util = __webpack_require__(824); /** * Determine whether mappingB is after mappingA with respect to generated @@ -94624,7 +94839,7 @@ exports.MappingList = MappingList; /***/ }), -/* 825 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94634,11 +94849,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(822); -var binarySearch = __webpack_require__(826); -var ArraySet = __webpack_require__(823).ArraySet; -var base64VLQ = __webpack_require__(820); -var quickSort = __webpack_require__(827).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; @@ -95712,7 +95927,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 826 */ +/* 828 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95829,7 +96044,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 827 */ +/* 829 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95949,7 +96164,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 828 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95959,8 +96174,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(819).SourceMapGenerator; -var util = __webpack_require__(822); +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). @@ -96368,17 +96583,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 829 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(830) -var resolveUrl = __webpack_require__(831) -var decodeUriComponent = __webpack_require__(832) -var urix = __webpack_require__(834) -var atob = __webpack_require__(835) +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) @@ -96676,7 +96891,7 @@ module.exports = { /***/ }), -/* 830 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96739,7 +96954,7 @@ void (function(root, factory) { /***/ }), -/* 831 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96757,13 +96972,13 @@ module.exports = resolveUrl /***/ }), -/* 832 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(833) +var decodeUriComponent = __webpack_require__(835) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96774,7 +96989,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 833 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96875,7 +97090,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 834 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96898,7 +97113,7 @@ module.exports = urix /***/ }), -/* 835 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96912,7 +97127,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 836 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96920,8 +97135,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(798); -var utils = __webpack_require__(817); +var define = __webpack_require__(800); +var utils = __webpack_require__(819); /** * Expose `mixin()`. @@ -97064,19 +97279,19 @@ exports.comment = function(node) { /***/ }), -/* 837 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(809); +var use = __webpack_require__(811); var util = __webpack_require__(29); -var Cache = __webpack_require__(838); -var define = __webpack_require__(798); -var debug = __webpack_require__(811)('snapdragon:parser'); -var Position = __webpack_require__(839); -var utils = __webpack_require__(817); +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`. @@ -97604,7 +97819,7 @@ module.exports = Parser; /***/ }), -/* 838 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97711,13 +97926,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 839 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(798); +var define = __webpack_require__(800); /** * Store position for a node @@ -97732,16 +97947,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 840 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(841); -var define = __webpack_require__(847); -var extend = __webpack_require__(848); -var not = __webpack_require__(850); +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; /** @@ -97894,10 +98109,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 841 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(842); +var parse = __webpack_require__(844); var types = parse.types; module.exports = function (re, opts) { @@ -97943,13 +98158,13 @@ function isRegExp (x) { /***/ }), -/* 842 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(843); -var types = __webpack_require__(844); -var sets = __webpack_require__(845); -var positions = __webpack_require__(846); +var util = __webpack_require__(845); +var types = __webpack_require__(846); +var sets = __webpack_require__(847); +var positions = __webpack_require__(848); module.exports = function(regexpStr) { @@ -98231,11 +98446,11 @@ module.exports.types = types; /***/ }), -/* 843 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(844); -var sets = __webpack_require__(845); +var types = __webpack_require__(846); +var sets = __webpack_require__(847); // All of these are private and only used by randexp. @@ -98348,7 +98563,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 844 */ +/* 846 */ /***/ (function(module, exports) { module.exports = { @@ -98364,10 +98579,10 @@ module.exports = { /***/ }), -/* 845 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(844); +var types = __webpack_require__(846); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -98452,10 +98667,10 @@ exports.anyChar = function() { /***/ }), -/* 846 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(844); +var types = __webpack_require__(846); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -98475,7 +98690,7 @@ exports.end = function() { /***/ }), -/* 847 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98488,8 +98703,8 @@ exports.end = function() { -var isobject = __webpack_require__(749); -var isDescriptor = __webpack_require__(763); +var isobject = __webpack_require__(751); +var isDescriptor = __webpack_require__(765); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -98520,14 +98735,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 848 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(849); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(851); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98587,7 +98802,7 @@ function isEnum(obj, key) { /***/ }), -/* 849 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98600,7 +98815,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98608,14 +98823,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 850 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(848); -var safe = __webpack_require__(841); +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. @@ -98687,14 +98902,14 @@ module.exports = toRegex; /***/ }), -/* 851 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(852); -var extglob = __webpack_require__(868); +var nanomatch = __webpack_require__(854); +var extglob = __webpack_require__(870); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98771,7 +98986,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 852 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98782,17 +98997,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(853); -var extend = __webpack_require__(854); +var toRegex = __webpack_require__(855); +var extend = __webpack_require__(856); /** * Local dependencies */ -var compilers = __webpack_require__(856); -var parsers = __webpack_require__(857); -var cache = __webpack_require__(860); -var utils = __webpack_require__(862); +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; /** @@ -99616,15 +99831,15 @@ module.exports = nanomatch; /***/ }), -/* 853 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(798); -var extend = __webpack_require__(740); -var not = __webpack_require__(739); +var define = __webpack_require__(800); +var extend = __webpack_require__(742); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -99771,14 +99986,14 @@ module.exports.makeRe = makeRe; /***/ }), -/* 854 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(855); -var assignSymbols = __webpack_require__(750); +var isExtendable = __webpack_require__(857); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99838,7 +100053,7 @@ function isEnum(obj, key) { /***/ }), -/* 855 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99851,7 +100066,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(748); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99859,7 +100074,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 856 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100205,15 +100420,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 857 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(853); -var isOdd = __webpack_require__(858); +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 @@ -100599,7 +100814,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 858 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100612,7 +100827,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(859); +var isNumber = __webpack_require__(861); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -100626,7 +100841,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 859 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100654,14 +100869,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 860 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(861))(); +module.exports = new (__webpack_require__(863))(); /***/ }), -/* 861 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100674,7 +100889,7 @@ module.exports = new (__webpack_require__(861))(); -var MapCache = __webpack_require__(838); +var MapCache = __webpack_require__(840); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100796,7 +101011,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 862 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100809,14 +101024,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(863)(); -var Snapdragon = __webpack_require__(771); -utils.define = __webpack_require__(864); -utils.diff = __webpack_require__(865); -utils.extend = __webpack_require__(854); -utils.pick = __webpack_require__(866); -utils.typeOf = __webpack_require__(867); -utils.unique = __webpack_require__(742); +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 @@ -101182,7 +101397,7 @@ utils.unixify = function(options) { /***/ }), -/* 863 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -101210,7 +101425,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 864 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101223,8 +101438,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(749); -var isDescriptor = __webpack_require__(763); +var isobject = __webpack_require__(751); +var isDescriptor = __webpack_require__(765); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -101255,7 +101470,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 865 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101309,7 +101524,7 @@ function diffArray(one, two) { /***/ }), -/* 866 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101322,7 +101537,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(749); +var isObject = __webpack_require__(751); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -101351,7 +101566,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 867 */ +/* 869 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -101486,7 +101701,7 @@ function isBuffer(val) { /***/ }), -/* 868 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101496,18 +101711,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(740); -var unique = __webpack_require__(742); -var toRegex = __webpack_require__(853); +var extend = __webpack_require__(742); +var unique = __webpack_require__(744); +var toRegex = __webpack_require__(855); /** * Local dependencies */ -var compilers = __webpack_require__(869); -var parsers = __webpack_require__(880); -var Extglob = __webpack_require__(883); -var utils = __webpack_require__(882); +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; /** @@ -101824,13 +102039,13 @@ module.exports = extglob; /***/ }), -/* 869 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(870); +var brackets = __webpack_require__(872); /** * Extglob compilers @@ -102000,7 +102215,7 @@ module.exports = function(extglob) { /***/ }), -/* 870 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102010,17 +102225,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(871); -var parsers = __webpack_require__(873); +var compilers = __webpack_require__(873); +var parsers = __webpack_require__(875); /** * Module dependencies */ -var debug = __webpack_require__(875)('expand-brackets'); -var extend = __webpack_require__(740); -var Snapdragon = __webpack_require__(771); -var toRegex = __webpack_require__(853); +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 @@ -102218,13 +102433,13 @@ module.exports = brackets; /***/ }), -/* 871 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(872); +var posix = __webpack_require__(874); module.exports = function(brackets) { brackets.compiler @@ -102312,7 +102527,7 @@ module.exports = function(brackets) { /***/ }), -/* 872 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102341,14 +102556,14 @@ module.exports = { /***/ }), -/* 873 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(874); -var define = __webpack_require__(798); +var utils = __webpack_require__(876); +var define = __webpack_require__(800); /** * Text regex @@ -102567,14 +102782,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 874 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(853); -var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(855); +var regexNot = __webpack_require__(741); var cached; /** @@ -102608,7 +102823,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 875 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102617,14 +102832,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(876); + module.exports = __webpack_require__(878); } else { - module.exports = __webpack_require__(879); + module.exports = __webpack_require__(881); } /***/ }), -/* 876 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102633,7 +102848,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(877); +exports = module.exports = __webpack_require__(879); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -102815,7 +103030,7 @@ function localstorage() { /***/ }), -/* 877 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { @@ -102831,7 +103046,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(878); +exports.humanize = __webpack_require__(880); /** * The currently active debug mode names, and names to skip. @@ -103023,7 +103238,7 @@ function coerce(val) { /***/ }), -/* 878 */ +/* 880 */ /***/ (function(module, exports) { /** @@ -103181,14 +103396,14 @@ function plural(ms, n, name) { /***/ }), -/* 879 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(478); +var tty = __webpack_require__(484); var util = __webpack_require__(29); /** @@ -103197,7 +103412,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(877); +exports = module.exports = __webpack_require__(879); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -103376,7 +103591,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(816); + var net = __webpack_require__(818); stream = new net.Socket({ fd: fd, readable: false, @@ -103435,15 +103650,15 @@ exports.enable(load()); /***/ }), -/* 880 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(870); -var define = __webpack_require__(881); -var utils = __webpack_require__(882); +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 @@ -103598,7 +103813,7 @@ module.exports = parsers; /***/ }), -/* 881 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103611,7 +103826,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(763); +var isDescriptor = __webpack_require__(765); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -103636,14 +103851,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 882 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(739); -var Cache = __webpack_require__(861); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(863); /** * Utils @@ -103712,7 +103927,7 @@ utils.createRegex = function(str) { /***/ }), -/* 883 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103722,16 +103937,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(771); -var define = __webpack_require__(881); -var extend = __webpack_require__(740); +var Snapdragon = __webpack_require__(773); +var define = __webpack_require__(883); +var extend = __webpack_require__(742); /** * Local dependencies */ -var compilers = __webpack_require__(869); -var parsers = __webpack_require__(880); +var compilers = __webpack_require__(871); +var parsers = __webpack_require__(882); /** * Customize Snapdragon parser and renderer @@ -103797,16 +104012,16 @@ module.exports = Extglob; /***/ }), -/* 884 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(868); -var nanomatch = __webpack_require__(852); -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(840); +var extglob = __webpack_require__(870); +var nanomatch = __webpack_require__(854); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(842); var not; /** @@ -103887,14 +104102,14 @@ function textRegex(pattern) { /***/ }), -/* 885 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(861))(); +module.exports = new (__webpack_require__(863))(); /***/ }), -/* 886 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103907,13 +104122,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(771); -utils.define = __webpack_require__(847); -utils.diff = __webpack_require__(865); -utils.extend = __webpack_require__(848); -utils.pick = __webpack_require__(866); -utils.typeOf = __webpack_require__(887); -utils.unique = __webpack_require__(742); +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 `\\`. @@ -104210,7 +104425,7 @@ utils.unixify = function(options) { /***/ }), -/* 887 */ +/* 889 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -104345,7 +104560,7 @@ function isBuffer(val) { /***/ }), -/* 888 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104364,9 +104579,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(889); -var reader_1 = __webpack_require__(902); -var fs_stream_1 = __webpack_require__(906); +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() { @@ -104427,15 +104642,15 @@ exports.default = ReaderAsync; /***/ }), -/* 889 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(890); -const readdirAsync = __webpack_require__(898); -const readdirStream = __webpack_require__(901); +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; @@ -104519,7 +104734,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 890 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104527,11 +104742,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(891); +const DirectoryReader = __webpack_require__(893); let syncFacade = { - fs: __webpack_require__(896), - forEach: __webpack_require__(897), + fs: __webpack_require__(898), + forEach: __webpack_require__(899), sync: true }; @@ -104560,7 +104775,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104569,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__(892); -const stat = __webpack_require__(894); -const call = __webpack_require__(895); +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 @@ -104947,14 +105162,14 @@ module.exports = DirectoryReader; /***/ }), -/* 892 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(893); +const globToRegExp = __webpack_require__(895); module.exports = normalizeOptions; @@ -105131,7 +105346,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 893 */ +/* 895 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -105268,13 +105483,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 894 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(895); +const call = __webpack_require__(897); module.exports = stat; @@ -105349,7 +105564,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 895 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105410,14 +105625,14 @@ function callOnce (fn) { /***/ }), -/* 896 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(895); +const call = __webpack_require__(897); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -105481,7 +105696,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 897 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105510,7 +105725,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 898 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105518,12 +105733,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(899); -const DirectoryReader = __webpack_require__(891); +const maybe = __webpack_require__(901); +const DirectoryReader = __webpack_require__(893); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(900), + forEach: __webpack_require__(902), async: true }; @@ -105565,7 +105780,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 899 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105592,7 +105807,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 900 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105628,7 +105843,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 901 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105636,11 +105851,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(891); +const DirectoryReader = __webpack_require__(893); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(900), + forEach: __webpack_require__(902), async: true }; @@ -105660,16 +105875,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 902 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(903); -var entry_1 = __webpack_require__(905); -var pathUtil = __webpack_require__(904); +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; @@ -105735,14 +105950,14 @@ exports.default = Reader; /***/ }), -/* 903 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(904); -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; @@ -105825,7 +106040,7 @@ exports.default = DeepFilter; /***/ }), -/* 904 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105856,14 +106071,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 905 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(904); -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; @@ -105948,7 +106163,7 @@ exports.default = EntryFilter; /***/ }), -/* 906 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105968,8 +106183,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(907); -var fs_1 = __webpack_require__(911); +var fsStat = __webpack_require__(909); +var fs_1 = __webpack_require__(913); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -106019,14 +106234,14 @@ exports.default = FileSystemStream; /***/ }), -/* 907 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(908); -const statProvider = __webpack_require__(910); +const optionsManager = __webpack_require__(910); +const statProvider = __webpack_require__(912); /** * Asynchronous API. */ @@ -106057,13 +106272,13 @@ exports.statSync = statSync; /***/ }), -/* 908 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(909); +const fsAdapter = __webpack_require__(911); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -106076,7 +106291,7 @@ exports.prepare = prepare; /***/ }), -/* 909 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106099,7 +106314,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 910 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106151,7 +106366,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 911 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106182,7 +106397,7 @@ exports.default = FileSystem; /***/ }), -/* 912 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106202,9 +106417,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(889); -var reader_1 = __webpack_require__(902); -var fs_stream_1 = __webpack_require__(906); +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) { @@ -106272,7 +106487,7 @@ exports.default = ReaderStream; /***/ }), -/* 913 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106291,9 +106506,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(889); -var reader_1 = __webpack_require__(902); -var fs_sync_1 = __webpack_require__(914); +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() { @@ -106353,7 +106568,7 @@ exports.default = ReaderSync; /***/ }), -/* 914 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106372,8 +106587,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(907); -var fs_1 = __webpack_require__(911); +var fsStat = __webpack_require__(909); +var fs_1 = __webpack_require__(913); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -106419,7 +106634,7 @@ exports.default = FileSystemSync; /***/ }), -/* 915 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106435,13 +106650,13 @@ exports.flatten = flatten; /***/ }), -/* 916 */ +/* 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. */ @@ -106456,13 +106671,13 @@ exports.merge = merge; /***/ }), -/* 917 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(918); +const pathType = __webpack_require__(920); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -106528,13 +106743,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 918 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(919); +const pify = __webpack_require__(921); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -106577,7 +106792,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 919 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106668,17 +106883,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 920 */ +/* 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__(921); -const pify = __webpack_require__(922); -const slash = __webpack_require__(923); +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/**', @@ -106776,7 +106991,7 @@ module.exports.sync = options => { /***/ }), -/* 921 */ +/* 923 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -107245,7 +107460,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 922 */ +/* 924 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107320,7 +107535,7 @@ module.exports = (input, options) => { /***/ }), -/* 923 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107338,17 +107553,17 @@ module.exports = input => { /***/ }), -/* 924 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(925); -const CpFileError = __webpack_require__(928); -const fs = __webpack_require__(932); -const ProgressEmitter = __webpack_require__(935); +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; @@ -107462,12 +107677,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 925 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(926); +const pTimeout = __webpack_require__(928); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -107758,12 +107973,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 926 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(927); +const pFinally = __webpack_require__(929); class TimeoutError extends Error { constructor(message) { @@ -107809,7 +108024,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 927 */ +/* 929 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107831,12 +108046,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 928 */ +/* 930 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(929); +const NestedError = __webpack_require__(931); class CpFileError extends NestedError { constructor(message, nested) { @@ -107850,10 +108065,10 @@ module.exports = CpFileError; /***/ }), -/* 929 */ +/* 931 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(930); +var inherits = __webpack_require__(932); var NestedError = function (message, nested) { this.nested = nested; @@ -107904,7 +108119,7 @@ module.exports = NestedError; /***/ }), -/* 930 */ +/* 932 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107912,12 +108127,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(931); + module.exports = __webpack_require__(933); } /***/ }), -/* 931 */ +/* 933 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107946,16 +108161,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 932 */ +/* 934 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(933); -const pEvent = __webpack_require__(925); -const CpFileError = __webpack_require__(928); +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); @@ -108052,7 +108267,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 933 */ +/* 935 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108060,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__(934); +const semver = __webpack_require__(936); const defaults = { mode: 0o777 & (~process.umask()), @@ -108209,7 +108424,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 934 */ +/* 936 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109811,7 +110026,7 @@ function coerce (version, options) { /***/ }), -/* 935 */ +/* 937 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109852,7 +110067,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 936 */ +/* 938 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109898,12 +110113,12 @@ exports.default = module.exports; /***/ }), -/* 937 */ +/* 939 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(938); +const NestedError = __webpack_require__(940); class CpyError extends NestedError { constructor(message, nested) { @@ -109917,7 +110132,7 @@ module.exports = CpyError; /***/ }), -/* 938 */ +/* 940 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109973,14 +110188,14 @@ module.exports = NestedError; /***/ }), -/* 939 */ +/* 941 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; }); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(516); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(515); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(518); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(517); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with diff --git a/packages/kbn-pm/src/utils/project_checksums.ts b/packages/kbn-pm/src/utils/project_checksums.ts index 572f2adb19bd9..7d939e715d411 100644 --- a/packages/kbn-pm/src/utils/project_checksums.ts +++ b/packages/kbn-pm/src/utils/project_checksums.ts @@ -32,7 +32,7 @@ import { Kibana } from '../utils/kibana'; export type ChecksumMap = Map; /** map of [repo relative path to changed file, type of change] */ -type Changes = Map; +type Changes = Map; const statAsync = promisify(Fs.stat); const projectBySpecificitySorter = (a: Project, b: Project) => b.path.length - a.path.length; @@ -45,7 +45,8 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too 'git', [ 'ls-files', - '-dmt', + '-dmto', + '--exclude-standard', '--', ...Array.from(projects.values()) .filter(p => kbn.isPartOfRepo(p)) @@ -78,10 +79,13 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too unassignedChanges.set(path, 'deleted'); break; + case '?': + unassignedChanges.set(path, 'untracked'); + break; + case 'H': case 'S': case 'K': - case '?': default: log.warning(`unexpected modification status "${tag}" for ${path}, please report this!`); unassignedChanges.set(path, 'invalid'); diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d..c0ddcaf4f23c8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -846,6 +846,14 @@ '@types/semver', ], }, + { + groupSlug: 'set-value', + groupName: 'set-value related packages', + packageNames: [ + 'set-value', + '@types/set-value', + ], + }, { groupSlug: 'sinon', groupName: 'sinon related packages', diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index a87e2aa11f2c0..32a23d74dbda4 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 b877847aaa90e..422442c9ca4d2 100644 --- a/src/core/public/plugins/plugin.test.mocks.ts +++ b/src/core/public/plugins/plugin.test.mocks.ts @@ -24,8 +24,8 @@ export const mockPlugin = { }; export const mockInitializer = jest.fn(() => mockPlugin); -export const mockPluginLoader = jest.fn().mockResolvedValue(mockInitializer); +export const mockPluginReader = jest.fn(() => mockInitializer); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockPluginLoader, +jest.mock('./plugin_reader', () => ({ + read: mockPluginReader, })); diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 39330711f7980..8fe745db9554d 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { mockInitializer, mockPlugin, mockPluginLoader } from './plugin.test.mocks'; +import { mockInitializer, mockPlugin, mockPluginReader } from './plugin.test.mocks'; import { DiscoveredPlugin } from '../../server'; import { coreMock } from '../mocks'; @@ -38,10 +38,9 @@ function createManifest( let plugin: PluginWrapper>; const opaqueId = Symbol(); const initializerContext = coreMock.createPluginInitializerContext(); -const addBasePath = (path: string) => path; beforeEach(() => { - mockPluginLoader.mockClear(); + mockPluginReader.mockClear(); mockPlugin.setup.mockClear(); mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); @@ -49,20 +48,8 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`load` calls loadPluginBundle', () => { - plugin.load(addBasePath); - expect(mockPluginLoader).toHaveBeenCalledWith(addBasePath, 'plugin-a'); - }); - - test('`setup` fails if load is not called first', async () => { - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin \\"plugin-a\\" can't be setup since its bundle isn't loaded."` - ); - }); - test('`setup` fails if plugin.setup is not a function', async () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); @@ -70,20 +57,17 @@ describe('PluginWrapper', () => { test('`setup` fails if plugin.start is not a function', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); test('`setup` calls initializer with initializer context', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(mockInitializer).toHaveBeenCalledWith(initializerContext); }); test('`setup` calls plugin.setup with context and dependencies', async () => { - await plugin.load(addBasePath); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; await plugin.setup(context, deps); @@ -91,14 +75,12 @@ describe('PluginWrapper', () => { }); test('`start` fails if setup is not called first', async () => { - await plugin.load(addBasePath); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); test('`start` calls plugin.start with context and dependencies', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -114,20 +96,21 @@ describe('PluginWrapper', () => { }; let startDependenciesResolved = false; - mockPluginLoader.mockResolvedValueOnce(() => ({ - setup: jest.fn(), - start: async () => { - // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. - await new Promise(resolve => setTimeout(resolve, 10)); - expect(startDependenciesResolved).toBe(false); - return pluginStartContract; - }, - })); - await plugin.load(addBasePath); + mockPluginReader.mockReturnValueOnce( + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + return pluginStartContract; + }), + stop: jest.fn(), + })) + ); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; - // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is // called. const startDependenciesCheck = plugin.startDependencies.then(res => { @@ -145,7 +128,6 @@ describe('PluginWrapper', () => { }); test('`stop` calls plugin.stop', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); await plugin.stop(); expect(mockPlugin.stop).toHaveBeenCalled(); @@ -153,7 +135,6 @@ describe('PluginWrapper', () => { test('`stop` does not fail if plugin.stop does not exist', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any); - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(() => plugin.stop()).not.toThrow(); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index e51c45040c452..591165fcd2839 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -21,7 +21,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; -import { loadPluginBundle } from './plugin_loader'; +import { read } from './plugin_reader'; import { CoreStart, CoreSetup } from '..'; /** @@ -69,7 +69,6 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private initializer?: PluginInitializer; private instance?: Plugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); @@ -86,18 +85,6 @@ export class PluginWrapper< this.optionalPlugins = discoveredPlugin.optionalPlugins; } - /** - * Loads the plugin's bundle into the browser. Should be called in parallel with all plugins - * using `Promise.all`. Must be called before `setup`. - * @param addBasePath Function that adds the base path to a string for plugin bundle path. - */ - public async load(addBasePath: (path: string) => string) { - this.initializer = await loadPluginBundle( - addBasePath, - this.name - ); - } - /** * Instantiates plugin and calls `setup` function exposed by the plugin initializer. * @param setupContext Context that consists of various core services tailored specifically @@ -146,11 +133,14 @@ export class PluginWrapper< } private async createPluginInstance() { - if (this.initializer === undefined) { - throw new Error(`Plugin "${this.name}" can't be setup since its bundle isn't loaded.`); - } - - const instance = this.initializer(this.initializerContext); + const initializer = read(this.name) as PluginInitializer< + TSetup, + TStart, + TPluginsSetup, + TPluginsStart + >; + + const instance = initializer(this.initializerContext); if (typeof instance.setup !== 'function') { throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`); diff --git a/src/core/public/plugins/plugin_loader.mock.ts b/src/core/public/plugins/plugin_loader.mock.ts deleted file mode 100644 index abdd9d4ddce2a..0000000000000 --- a/src/core/public/plugins/plugin_loader.mock.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginName } from 'src/core/server'; -import { LoadPluginBundle, UnknownPluginInitializer } from './plugin_loader'; - -/** - * @param initializerProvider A function provided by the test to resolve initializers. - */ -const createLoadPluginBundleMock = ( - initializerProvider: (name: PluginName) => UnknownPluginInitializer -): jest.Mock, Parameters> => - jest.fn((addBasePath, pluginName, _ = {}) => { - return Promise.resolve(initializerProvider(pluginName)) as any; - }); - -export const loadPluginBundleMock = { create: createLoadPluginBundleMock }; diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts deleted file mode 100644 index b4e2c3095f14a..0000000000000 --- a/src/core/public/plugins/plugin_loader.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { CoreWindow, loadPluginBundle } from './plugin_loader'; - -let createdScriptTags = [] as any[]; -let appendChildSpy: jest.SpyInstance; -let createElementSpy: jest.SpyInstance< - HTMLElement, - [string, (ElementCreationOptions | undefined)?] ->; - -const coreWindow = (window as unknown) as CoreWindow; - -beforeEach(() => { - // Mock document.createElement to return fake tags we can use to inspect what - // loadPluginBundles does. - createdScriptTags = []; - createElementSpy = jest.spyOn(document, 'createElement').mockImplementation(() => { - const scriptTag = { setAttribute: jest.fn() } as any; - createdScriptTags.push(scriptTag); - return scriptTag; - }); - - // Mock document.body.appendChild to avoid errors about appending objects that aren't `Node`'s - // and so we can verify that the script tags were added to the page. - appendChildSpy = jest.spyOn(document.body, 'appendChild').mockReturnValue({} as any); - - // Mock global fields needed for loading modules. - coreWindow.__kbnBundles__ = {}; -}); - -afterEach(() => { - appendChildSpy.mockRestore(); - createElementSpy.mockRestore(); - delete coreWindow.__kbnBundles__; -}); - -const addBasePath = (path: string) => path; - -test('`loadPluginBundles` creates a script tag and loads initializer', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/bundles/plugin/plugin-a/plugin-a.plugin.js' - ); - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); - expect(fakeScriptTag.onload).toBeInstanceOf(Function); - expect(fakeScriptTag.onerror).toBeInstanceOf(Function); - expect(appendChildSpy).toHaveBeenCalledWith(fakeScriptTag); - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - const fakeInitializer = jest.fn(); - coreWindow.__kbnBundles__['plugin/plugin-a'] = { plugin: fakeInitializer }; - // Call the onload callback - fakeScriptTag.onload(); - await expect(loadPromise).resolves.toEqual(fakeInitializer); -}); - -test('`loadPluginBundles` includes the basePath', async () => { - loadPluginBundle((path: string) => `/mybasepath${path}`, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/mybasepath/bundles/plugin/plugin-a/plugin-a.plugin.js' - ); -}); - -test('`loadPluginBundles` rejects if script.onerror is called', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - const fakeScriptTag1 = createdScriptTags[0]; - // Call the error on the second script - fakeScriptTag1.onerror(new Error('Whoa there!')); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if timeout is reached', async () => { - await expect( - // Override the timeout to 1 ms for testi. - loadPluginBundle(addBasePath, 'plugin-a', { timeoutMs: 1 }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if bundle does attach an initializer to window.__kbnBundles__', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - const fakeScriptTag1 = createdScriptTags[0]; - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - coreWindow.__kbnBundles__['plugin/plugin-a'] = undefined; - // Call the onload callback - fakeScriptTag1.onload(); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a/plugin-a.plugin.js)."` - ); -}); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts deleted file mode 100644 index bf7711055e97b..0000000000000 --- a/src/core/public/plugins/plugin_loader.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginName } from '../../server'; -import { PluginInitializer } from './plugin'; - -/** - * Unknown variant for internal use only for when plugins are not known. - * @internal - */ -export type UnknownPluginInitializer = PluginInitializer>; - -/** - * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. - * @internal - */ -export interface CoreWindow { - __kbnBundles__: { - [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; - }; -} - -/** - * Timeout for loading a single script in milliseconds. - * @internal - */ -export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes - -/** - * Loads the bundle for a plugin onto the page and returns their PluginInitializer. This should - * be called for all plugins (once per plugin) in parallel using Promise.all. - * - * If this is slowing down browser load time, there are some ways we could make this faster: - * - Add these bundles in the generated bootstrap.js file so they're loaded immediately - * - Concatenate all the bundles files on the backend and serve them in single request. - * - Use HTTP/2 to load these bundles without having to open new connections for each. - * - * This may not be much of an issue since these should be cached by the browser after the first - * page load. - * - * @param basePath - * @param plugins - * @internal - */ -export const loadPluginBundle: LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - { timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {} -) => - new Promise>( - (resolve, reject) => { - const coreWindow = (window as unknown) as CoreWindow; - const exportId = `plugin/${pluginName}`; - - const readPluginExport = () => { - const PluginExport: any = coreWindow.__kbnBundles__[exportId]; - if (typeof PluginExport?.plugin !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve( - PluginExport.plugin as PluginInitializer - ); - } - }; - - if (coreWindow.__kbnBundles__[exportId]) { - readPluginExport(); - return; - } - - const script = document.createElement('script'); - // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); - script.setAttribute('src', bundlePath); - script.setAttribute('id', `kbn-plugin-${pluginName}`); - script.setAttribute('async', ''); - - const cleanupTag = () => { - clearTimeout(timeout); - // Set to null for IE memory leak issue. Webpack does the same thing. - // @ts-ignore - script.onload = script.onerror = null; - }; - - // Wire up resolve and reject - script.onload = () => { - cleanupTag(); - readPluginExport(); - }; - - script.onerror = () => { - cleanupTag(); - reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`)); - }; - - const timeout = setTimeout(() => { - cleanupTag(); - reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`)); - }, timeoutMs); - - // Add the script tag to the end of the body to start downloading - document.body.appendChild(script); - } - ); - -/** - * @internal - */ -export type LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - options?: { timeoutMs?: number } -) => Promise>; diff --git a/src/core/public/plugins/plugin_reader.test.ts b/src/core/public/plugins/plugin_reader.test.ts new file mode 100644 index 0000000000000..d4324f81de8e6 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.test.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreWindow, read, UnknownPluginInitializer } from './plugin_reader'; + +const coreWindow: CoreWindow = window as any; +beforeEach(() => { + coreWindow.__kbnBundles__ = {}; +}); + +it('handles undefined plugin exports', () => { + coreWindow.__kbnBundles__['plugin/foo'] = undefined; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" not found and may have failed to load.`); +}); + +it('handles plugin exports with a "plugin" export that is not a function', () => { + coreWindow.__kbnBundles__['plugin/foo'] = { + plugin: 1234, + } as any; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" should be a function.`); +}); + +it('returns the plugin initializer when the "plugin" named export is a function', () => { + const plugin: UnknownPluginInitializer = () => { + return undefined as any; + }; + + coreWindow.__kbnBundles__['plugin/foo'] = { plugin }; + + expect(read('foo')).toBe(plugin); +}); diff --git a/src/core/public/plugins/plugin_reader.ts b/src/core/public/plugins/plugin_reader.ts new file mode 100644 index 0000000000000..1907dfa6a3e99 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from './plugin'; + +/** + * Unknown variant for internal use only for when plugins are not known. + * @internal + */ +export type UnknownPluginInitializer = PluginInitializer>; + +/** + * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. + * @internal + */ +export interface CoreWindow { + __kbnBundles__: { + [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; + }; +} + +/** + * Reads the plugin's bundle declared in the global context. + */ +export function read(name: string) { + const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${name}`; + const pluginExport = coreWindow.__kbnBundles__[exportId]; + if (!pluginExport) { + throw new Error(`Definition of plugin "${name}" not found and may have failed to load.`); + } else if (typeof pluginExport.plugin !== 'function') { + throw new Error(`Definition of plugin "${name}" should be a function.`); + } else { + return pluginExport.plugin; + } +} diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index a76078932518f..85b84e561056a 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -19,16 +19,16 @@ import { PluginName } from 'kibana/server'; import { Plugin } from './plugin'; -import { loadPluginBundleMock } from './plugin_loader.mock'; export type MockedPluginInitializer = jest.Mock>, any>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, [PluginName] -> = jest.fn().mockRejectedValue(new Error('No provider specified')); +> = jest.fn().mockImplementation(() => () => { + throw new Error('No provider specified'); +}); -export const mockLoadPluginBundle = loadPluginBundleMock.create(mockPluginInitializerProvider); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockLoadPluginBundle, +jest.mock('./plugin_reader', () => ({ + read: mockPluginInitializerProvider, })); diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 688eaf4f2bfc7..6d71844bc19c8 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -21,7 +21,6 @@ import { omit, pick } from 'lodash'; import { MockedPluginInitializer, - mockLoadPluginBundle, mockPluginInitializerProvider, } from './plugins_service.test.mocks'; @@ -32,6 +31,7 @@ import { PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; + import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; @@ -152,10 +152,6 @@ describe('PluginsService', () => { ] as unknown) as [[PluginName, any]]); }); - afterEach(() => { - mockLoadPluginBundle.mockClear(); - }); - describe('#getOpaqueIds()', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -174,15 +170,6 @@ describe('PluginsService', () => { }); describe('#setup()', () => { - it('fails if any bundle cannot be loaded', async () => { - mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); - }); - it('fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -191,25 +178,6 @@ describe('PluginsService', () => { ); }); - it('calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginA' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginB' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginC' - ); - }); - it('initializes plugins with PluginInitializerContext', async () => { const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); @@ -302,7 +270,6 @@ describe('PluginsService', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); const promise = pluginsService.setup(mockSetupDeps); - jest.runAllTimers(); // load plugin bundles await flushPromises(); jest.runAllTimers(); // setup plugins diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index e698af689036d..862aa5043ad4b 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -93,9 +93,6 @@ export class PluginsService implements CoreService { - // Load plugin bundles - await this.loadPluginBundles(deps.http.basePath.prepend); - // Setup each plugin with required and optional plugin contracts const contracts = new Map(); for (const [pluginName, plugin] of this.plugins.entries()) { @@ -167,9 +164,4 @@ export class PluginsService implements CoreService string) { - // Load all bundles in parallel - return Promise.all([...this.plugins.values()].map(plugin => plugin.load(addBasePath))); - } } diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts index 1790b096a71ae..dfa2396d5904b 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts @@ -48,7 +48,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); @@ -65,7 +65,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts index f9c3dcbf554cb..df86f5a2b4031 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts @@ -22,9 +22,10 @@ import { LegacyPluginSpec } from '../types'; const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; -const breakingChangesUrl = - 'https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html'; -const migrationGuideUrl = 'https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md'; +// Use shortened URLs so destinations can be updated if/when documentation moves +// All platform team members have access to edit these +const breakingChangesUrl = 'https://ela.st/kibana-breaking-changes-8-0'; +const migrationGuideUrl = 'https://ela.st/kibana-platform-migration'; export const logLegacyThirdPartyPluginDeprecationWarning = ({ specs, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2451b98ffdf29..c707fa2b479e4 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -18,7 +18,7 @@ */ import { of } from 'rxjs'; import { duration } from 'moment'; -import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; +import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; import { CspConfig } from './csp'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -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 b38259a84cb9c..73e119a5a97e7 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -104,6 +104,10 @@ export const Template: FunctionComponent = ({ + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} + + {createElement('kbn-csp', { diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 47fc29f8cf7d2..c1b65763949bb 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -131,6 +131,7 @@ export interface IndexMappingMeta { */ export interface SavedObjectsCoreFieldMapping { type: string; + null_value?: number | boolean | string; index?: boolean; enabled?: boolean; fields?: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a36e746f6d940..dc1c9d379d508 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1840,6 +1840,8 @@ export interface SavedObjectsCoreFieldMapping { // (undocumented) index?: boolean; // (undocumented) + null_value?: number | boolean | string; + // (undocumented) type: string; } diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.js b/src/dev/build/tasks/build_kibana_platform_plugins.js index 79cd698e4782c..28d6b49f9e89a 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.js +++ b/src/dev/build/tasks/build_kibana_platform_plugins.js @@ -17,7 +17,13 @@ * under the License. */ -import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; +import { CiStatsReporter } from '@kbn/dev-utils'; +import { + runOptimizer, + OptimizerConfig, + logOptimizerState, + reportOptimizerStats, +} from '@kbn/optimizer'; export const BuildKibanaPlatformPluginsTask = { description: 'Building distributable versions of Kibana platform plugins', @@ -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 eb7a121c2e64b..d1fb544de733c 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -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 43a2cbd78c502..c5387590fcf66 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -40,6 +40,7 @@ export default { ], collectCoverageFrom: [ 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index fc95288eabed8..66296736b3ad0 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/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 21b7ea7dbf4c3..9f5f4b764f9b0 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 05cea7addf560..8a542fec0639c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js @@ -20,13 +20,10 @@ import _ from 'lodash'; import $ from 'jquery'; -import { Vis } from '../../../vis'; +import { Vis } from '../../../../../../plugins/vis_type_vislib/public/vislib/vis'; // TODO: Remove when converted to jest mocks -import { - ColorsService, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/charts/public/services'; +import { ColorsService } from '../../../../../../plugins/charts/public/services'; const $visCanvas = $('
') .attr('id', 'vislib-vis-fixtures') @@ -72,17 +69,6 @@ const getDeps = () => { }; }; -export const getMockUiState = () => { - const map = new Map(); - - return (() => ({ - get: (...args) => map.get(...args), - set: (...args) => map.set(...args), - setSilent: (...args) => map.set(...args), - on: () => undefined, - }))(); -}; - export function getVis(visLibParams, element) { return new Vis( element || $visCanvas.new(), diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js similarity index 91% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js index b65571becd83c..81fef155daf57 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js @@ -21,9 +21,9 @@ import d3 from 'd3'; import _ from 'lodash'; import expect from '@kbn/expect'; -import { ChartTitle } from '../../lib/chart_title'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; +import { ChartTitle } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/chart_title'; +import { VisConfig } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; describe('Vislib ChartTitle Class Test Suite', function() { let mockUiState; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js index a5d8eb80419a1..eb4e109690c37 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js @@ -22,9 +22,10 @@ import d3 from 'd3'; import expect from '@kbn/expect'; // Data -import data from './fixtures/mock_data/date_histogram/_series'; +import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import { getVis, getMockUiState } from './fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib Dispatch Class Test Suite', function() { function destroyVis(vis) { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js new file mode 100644 index 0000000000000..27f7f4ed3e073 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import $ from 'jquery'; + +// Data +import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../../_vis_fixture'; + +const dateHistogramArray = [series, columns, rows, stackedSeries]; +const names = ['series', 'columns', 'rows', 'stackedSeries']; + +dateHistogramArray.forEach(function(data, i) { + describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function() { + const events = ['click', 'brush']; + let vis; + + beforeEach(() => { + vis = getVis(); + vis.render(data, getMockUiState()); + }); + + afterEach(function() { + vis.destroy(); + }); + + describe('render Method', function() { + it('should render charts', function() { + expect(vis.handler.charts.length).to.be.greaterThan(0); + vis.handler.charts.forEach(function(chart) { + expect($(chart.chartEl).find('svg').length).to.be(1); + }); + }); + }); + + describe('enable Method', function() { + let charts; + + beforeEach(function() { + charts = vis.handler.charts; + + charts.forEach(function(chart) { + events.forEach(function(event) { + vis.handler.enable(event, chart); + }); + }); + }); + + it('should add events to chart and emit to the Events class', function() { + charts.forEach(function(chart) { + events.forEach(function(event) { + expect(chart.events.listenerCount(event)).to.be.above(0); + }); + }); + }); + }); + + describe('disable Method', function() { + let charts; + + beforeEach(function() { + charts = vis.handler.charts; + + charts.forEach(function(chart) { + events.forEach(function(event) { + vis.handler.disable(event, chart); + }); + }); + }); + + it('should remove events from the chart', function() { + charts.forEach(function(chart) { + events.forEach(function(event) { + expect(chart.events.listenerCount(event)).to.be(0); + }); + }); + }); + }); + + describe('removeAll Method', function() { + beforeEach(function() { + vis.handler.removeAll(vis.element); + }); + + it('should remove all DOM elements from the el', function() { + expect($(vis.element).children().length).to.be(0); + }); + }); + + describe('error Method', function() { + beforeEach(function() { + vis.handler.error('This is an error!'); + }); + + it('should return an error classed DOM element with a text message', function() { + expect($(vis.element).find('.error').length).to.be(1); + expect($('.error h4').html()).to.be('This is an error!'); + }); + }); + + describe('destroy Method', function() { + beforeEach(function() { + vis.handler.destroy(); + }); + + it('should destroy all the charts in the visualization', function() { + expect(vis.handler.charts.length).to.be(0); + }); + }); + + describe('event proxying', function() { + it('should only pass the original event object to downstream handlers', function(done) { + const event = {}; + const chart = vis.handler.charts[0]; + + const mockEmitter = function() { + const args = Array.from(arguments); + expect(args.length).to.be(2); + expect(args[0]).to.be('click'); + expect(args[1]).to.be(event); + done(); + }; + + vis.emit = mockEmitter; + vis.handler.enable('click', chart); + chart.events.emit('click', event); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js similarity index 85% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js index f72794e27e834..505b0a04c6183 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js @@ -22,14 +22,14 @@ import expect from '@kbn/expect'; import $ from 'jquery'; // Data -import series from '../fixtures/mock_data/date_histogram/_series'; -import columns from '../fixtures/mock_data/date_histogram/_columns'; -import rows from '../fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; - -import { Layout } from '../../../lib/layout/layout'; -import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; -import { VisConfig } from '../../../lib/vis_config'; +import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { Layout } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/layout/layout'; +import { VisConfig } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; +import { getVis } from '../../_vis_fixture'; const dateHistogramArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js similarity index 92% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js index 4852f71d8c45b..67f29ee96a336 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js @@ -21,11 +21,12 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import series from './lib/fixtures/mock_data/date_histogram/_series'; -import columns from './lib/fixtures/mock_data/date_histogram/_columns'; -import rows from './lib/fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from './lib/fixtures/mock_data/date_histogram/_stacked_series'; -import { getVis, getMockUiState } from './lib/fixtures/_vis_fixture'; +import series from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from './_vis_fixture'; const dataArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js index c3f5859eb454c..eb529c380cdda 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js @@ -22,15 +22,16 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; const dataTypesArray = { - 'series pos': require('../lib/fixtures/mock_data/date_histogram/_series'), - 'series pos neg': require('../lib/fixtures/mock_data/date_histogram/_series_pos_neg'), - 'series neg': require('../lib/fixtures/mock_data/date_histogram/_series_neg'), - 'term columns': require('../lib/fixtures/mock_data/terms/_columns'), - 'range rows': require('../lib/fixtures/mock_data/range/_rows'), - stackedSeries: require('../lib/fixtures/mock_data/date_histogram/_stacked_series'), + 'series pos': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'), + 'series pos neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'), + 'series neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'), + 'term columns': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'), + 'range rows': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'), + stackedSeries: require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'), }; const visLibParams = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js similarity index 92% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js index 9653f9abab6fb..4c5e3db316243 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js @@ -20,8 +20,9 @@ import d3 from 'd3'; import expect from '@kbn/expect'; -import { Chart } from '../../visualizations/_chart'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { Chart } from '../../../../../../../plugins/vis_type_vislib/public/vislib/visualizations/_chart'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib _chart Test Suite', function() { let vis; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js index 2216294fcbac1..5cbd5948bc477 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js @@ -23,17 +23,18 @@ import $ from 'jquery'; import expect from '@kbn/expect'; // Data -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; -import histogramRows from '../lib/fixtures/mock_data/histogram/_rows'; -import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; - -import { seriesMonthlyInterval } from '../lib/fixtures/mock_data/date_histogram/_series_monthly_interval'; -import { rowsSeriesWithHoles } from '../lib/fixtures/mock_data/date_histogram/_rows_series_with_holes'; -import rowsWithZeros from '../lib/fixtures/mock_data/date_histogram/_rows'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; +import histogramRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows'; +import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; + +import { seriesMonthlyInterval } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval'; +import { rowsSeriesWithHoles } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes'; +import rowsWithZeros from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; // tuple, with the format [description, mode, data] const dataTypesArray = [ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js index fe25734fcbfde..d8ce8f1f5f44b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js @@ -21,8 +21,9 @@ import $ from 'jquery'; import _ from 'lodash'; import expect from '@kbn/expect'; -import data from '../lib/fixtures/mock_data/terms/_seriesMultiple'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function() { let vis; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js new file mode 100644 index 0000000000000..765b9118e6844 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import $ from 'jquery'; +import d3 from 'd3'; +import expect from '@kbn/expect'; + +// Data +import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; +import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; + +// tuple, with the format [description, mode, data] +const dataTypesArray = [ + ['series', series], + ['series with positive and negative values', seriesPosNeg], + ['series with negative values', seriesNeg], + ['terms columns', termsColumns], + ['stackedSeries', stackedSeries], +]; + +describe('Vislib Heatmap Chart Test Suite', function() { + dataTypesArray.forEach(function(dataType) { + const name = dataType[0]; + const data = dataType[1]; + + describe('for ' + name + ' Data', function() { + let vis; + let mockUiState; + const visLibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + invertColors: false, + colorsRange: [], + }; + + function generateVis(opts = {}) { + const config = _.defaultsDeep({}, opts, visLibParams); + vis = getVis(config); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(data, mockUiState); + } + + beforeEach(() => { + generateVis(); + }); + + afterEach(function() { + vis.destroy(); + }); + + it('category axes should be rendered in reverse order', () => { + const renderedCategoryAxes = vis.handler.renderArray.filter(item => { + return ( + item.constructor && + item.constructor.name === 'Axis' && + item.axisConfig.get('type') === 'category' + ); + }); + expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length); + expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal( + renderedCategoryAxes[1].axisConfig.get('id') + ); + expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal( + renderedCategoryAxes[0].axisConfig.get('id') + ); + }); + + describe('addSquares method', function() { + it('should append rects', function() { + vis.handler.charts.forEach(function(chart) { + const numOfRects = chart.chartData.series.reduce((result, series) => { + return result + series.values.length; + }, 0); + expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects); + }); + }); + }); + + describe('addBarEvents method', function() { + function checkChart(chart) { + const rect = $(chart.chartEl) + .find('.series rect') + .get(0); + + return { + click: !!rect.__onclick, + mouseOver: !!rect.__onmouseover, + // D3 brushing requires that a g element is appended that + // listens for mousedown events. This g element includes + // listeners, however, I was not able to test for the listener + // function being present. I will need to update this test + // in the future. + brush: !!d3.select('.brush')[0][0], + }; + } + + it('should attach the brush if data is a set of ordered dates', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + const ordered = vis.handler.data.get('ordered'); + const date = Boolean(ordered && ordered.date); + expect(has.brush).to.be(date); + }); + }); + + it('should attach a click event', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + expect(has.click).to.be(true); + }); + }); + + it('should attach a hover event', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + expect(has.mouseOver).to.be(true); + }); + }); + }); + + describe('draw method', function() { + it('should return a function', function() { + vis.handler.charts.forEach(function(chart) { + expect(_.isFunction(chart.draw())).to.be(true); + }); + }); + + it('should return a yMin and yMax', function() { + vis.handler.charts.forEach(function(chart) { + const yAxis = chart.handler.valueAxes[0]; + const domain = yAxis.getScale().domain(); + + expect(domain[0]).to.not.be(undefined); + expect(domain[1]).to.not.be(undefined); + }); + }); + }); + + it('should define default colors', function() { + expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); + }); + + it('should set custom range', function() { + vis.destroy(); + generateVis({ + setColorRange: true, + colorsRange: [ + { from: 0, to: 200 }, + { from: 200, to: 400 }, + { from: 400, to: 500 }, + { from: 500, to: Infinity }, + ], + }); + const labels = vis.getLegendLabels(); + expect(labels[0]).to.be('0 - 200'); + expect(labels[1]).to.be('200 - 400'); + expect(labels[2]).to.be('400 - 500'); + expect(labels[3]).to.be('500 - Infinity'); + }); + + it('should show correct Y axis title', function() { + expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal(''); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js index 1269fe7bcf62e..691417e968eed 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js @@ -23,14 +23,14 @@ import $ from 'jquery'; import _ from 'lodash'; // Data -import seriesPos from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import histogramColumns from '../lib/fixtures/mock_data/histogram/_columns'; -import rangeRows from '../lib/fixtures/mock_data/range/_rows'; -import termSeries from '../lib/fixtures/mock_data/terms/_series'; - -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import seriesPos from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import histogramColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns'; +import rangeRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'; +import termSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; const dataTypes = [ ['series pos', seriesPos], diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js index caafb2c636271..506ad2af85c34 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js @@ -22,7 +22,8 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; import { pieChartMockData } from './pie_chart_mock_data'; const names = ['rows', 'columns', 'slices']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart_mock_data.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart_mock_data.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 6ccbc13aeeb57..6366466103652 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -21,6 +21,8 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart } from 'kibana/public'; diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index d49c59970f521..26805554370b9 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -7,16 +7,9 @@ // Public UI styles @import 'src/legacy/ui/public/index'; -// vis_type_vislib UI styles -@import 'src/legacy/core_plugins/vis_type_vislib/public/index'; - // Discover styles @import 'discover/index'; -// Visualization styles are imported here for running karma Browser tests -// should be somehow included through the "visualizations" plugin initialization -@import '../../../../plugins/visualizations/public/index'; - // Has to come after visualize because of some // bad cascading in the Editor layout @import '../../../../plugins/maps_legacy/public/index'; diff --git a/src/legacy/core_plugins/kibana/public/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 4839870f0f3c8..564f115cf2c48 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 e05aea3678fe2..e2f387c0291a7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js @@ -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 6e1b0b7160941..87592cf4e750e 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -54,6 +54,7 @@ import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_ty import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -101,7 +102,7 @@ describe('RegionMapsVisualizationTests', function() { let getManifestStub; beforeEach( - ngMock.inject((Private, $injector) => { + ngMock.inject(() => { setInjectedVarFunc(injectedVar => { switch (injectedVar) { case 'mapConfig': @@ -127,17 +128,28 @@ describe('RegionMapsVisualizationTests', function() { } }); const serviceSettings = new ServiceSettings(); - const uiSettings = $injector.get('config'); const regionmapsConfig = { includeElasticMapsService: true, layers: [], }; + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: { + get: () => {}, + }, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; + const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); dependencies = { serviceSettings, - $injector, regionmapsConfig, - uiSettings, + uiSettings: coreSetupMock.uiSettings, + BaseMapsVisualization, }; regionMapVisType = new BaseVisType(createRegionMapTypeDefinition(dependencies)); diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 31a27c4da7fcf..5604067433f13 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -32,8 +32,7 @@ import { SelectOption, SwitchOption, } from '../../../../../plugins/charts/public'; -import { WmsOptions } from '../../../tile_map/public/components/wms_options'; -import { RegionMapVisParams } from '../types'; +import { RegionMapVisParams, WmsOptions } from '../../../../../plugins/maps_legacy/public'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, diff --git a/src/legacy/core_plugins/region_map/public/legacy.ts b/src/legacy/core_plugins/region_map/public/legacy.ts index b0cc767a044e8..4bbd839331e56 100644 --- a/src/legacy/core_plugins/region_map/public/legacy.ts +++ b/src/legacy/core_plugins/region_map/public/legacy.ts @@ -21,17 +21,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { RegionMapPluginSetupDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, mapsLegacy: npSetup.plugins.mapsLegacy, - - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index 1453c2155e2d6..08a73517dc13b 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -25,28 +25,28 @@ import { } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; - // @ts-ignore import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; +import { + getBaseMapsVis, + IServiceSettings, + MapsLegacyPluginSetup, +} from '../../../../plugins/maps_legacy/public'; /** @private */ -interface RegionMapVisualizationDependencies extends LegacyDependenciesPluginSetup { +interface RegionMapVisualizationDependencies { uiSettings: IUiSettingsClient; regionmapsConfig: RegionMapsConfig; serviceSettings: IServiceSettings; - notificationService: any; + BaseMapsVisualization: any; } /** @internal */ export interface RegionMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; - __LEGACY: LegacyDependenciesPlugin; mapsLegacy: MapsLegacyPluginSetup; } @@ -66,14 +66,13 @@ export class RegionMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, mapsLegacy, __LEGACY }: RegionMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy }: RegionMapPluginSetupDependencies ) { const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, regionmapsConfig: core.injectedMetadata.getInjectedVar('regionmap') as RegionMapsConfig, serviceSettings: mapsLegacy.serviceSettings, - notificationService: core.notifications.toasts, - ...(await __LEGACY.setup()), + BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), }; expressions.registerFunction(createRegionMapFn); diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 9174b03cf843c..b7ed14ed3706e 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -23,9 +23,7 @@ import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; import { Schemas } from '../../../../plugins/vis_default_editor/public'; - -// TODO: reference to TILE_MAP plugin should be removed -import { ORIGIN } from '../../tile_map/common/origin'; +import { ORIGIN } from '../../../../plugins/maps_legacy/public'; export function createRegionMapTypeDefinition(dependencies) { const { uiSettings, regionmapsConfig, serviceSettings } = dependencies; diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index f08d53ee35c8d..5dbc1ecad277f 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -21,30 +21,21 @@ import { i18n } from '@kbn/i18n'; import ChoroplethLayer from './choropleth_layer'; import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { toastNotifications } from 'ui/notify'; - -import { TileMapTooltipFormatter } from './tooltip_formatter'; import { truncatedColorMaps } from '../../../../plugins/charts/public'; - -// TODO: reference to TILE_MAP plugin should be removed -import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; +import { tooltipFormatter } from './tooltip_formatter'; +import { mapTooltipProvider } from '../../../../plugins/maps_legacy/public'; export function createRegionMapVisualization({ serviceSettings, - $injector, uiSettings, - notificationService, + BaseMapsVisualization, }) { - const BaseMapsVisualization = new BaseMapsVisualizationProvider( - serviceSettings, - notificationService - ); - const tooltipFormatter = new TileMapTooltipFormatter($injector); - return class RegionMapsVisualization extends BaseMapsVisualization { constructor(container, vis) { super(container, vis); this._vis = this.vis; this._choroplethLayer = null; + this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter); } async render(esResponse, visParams) { @@ -89,7 +80,7 @@ export function createRegionMapVisualization({ this._choroplethLayer.setMetrics(results, metricFieldFormatter, valueColumn.name); if (termColumn && valueColumn) { this._choroplethLayer.setTooltipFormatter( - tooltipFormatter, + this._tooltipFormatter, metricFieldFormatter, termColumn.name, valueColumn.name @@ -123,7 +114,7 @@ export function createRegionMapVisualization({ this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); this._choroplethLayer.setLineWeight(visParams.outlineWeight); this._choroplethLayer.setTooltipFormatter( - tooltipFormatter, + this._tooltipFormatter, metricFieldFormatter, this._metricLabel ); diff --git a/src/legacy/core_plugins/region_map/public/shim/index.ts b/src/legacy/core_plugins/region_map/public/shim/index.ts deleted file mode 100644 index cfc7b62ff4f86..0000000000000 --- a/src/legacy/core_plugins/region_map/public/shim/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './legacy_dependencies_plugin'; diff --git a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts deleted file mode 100644 index 3a7615e83f281..0000000000000 --- a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import chrome from 'ui/chrome'; -import { CoreStart, Plugin } from 'kibana/public'; - -/** @internal */ -export interface LegacyDependenciesPluginSetup { - $injector: any; - serviceSettings: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup() { - const $injector = await chrome.dangerouslyGetActiveInjector(); - - return { - $injector, - } as LegacyDependenciesPluginSetup; - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/region_map/public/tooltip.html b/src/legacy/core_plugins/region_map/public/tooltip.html deleted file mode 100644 index 0d57120c80a98..0000000000000 --- a/src/legacy/core_plugins/region_map/public/tooltip.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -
{{detail.label}}{{detail.value}}
diff --git a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js index 6df08aea0baa6..8d38095ac25e0 100644 --- a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js +++ b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js @@ -17,39 +17,24 @@ * under the License. */ -import $ from 'jquery'; -import template from './tooltip.html'; - -export const TileMapTooltipFormatter = $injector => { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(template); - - $compile($el)($tooltipScope); - - return function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { - if (!metric) { - return ''; - } - - $tooltipScope.details = []; - if (fieldName && metric) { - $tooltipScope.details.push({ - label: fieldName, - value: metric.term, - }); - } - - if (metric) { - $tooltipScope.details.push({ - label: metricName, - value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value, - }); - } - - $tooltipScope.$apply(); - return $el.html(); - }; -}; +export function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { + if (!metric) { + return ''; + } + + const details = []; + if (fieldName && metric) { + details.push({ + label: fieldName, + value: metric.term, + }); + } + + if (metric) { + details.push({ + label: metricName, + value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value, + }); + } + return details; +} diff --git a/src/legacy/core_plugins/region_map/public/types.ts b/src/legacy/core_plugins/region_map/public/types.ts deleted file mode 100644 index 8585bf720e0cf..0000000000000 --- a/src/legacy/core_plugins/region_map/public/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { VectorLayer, FileLayerField } from '../../../../plugins/maps_legacy/public'; -import { WMSOptions } from '../../tile_map/public/types'; - -export interface RegionMapVisParams { - readonly addTooltip: true; - readonly legendPosition: 'bottomright'; - colorSchema: string; - emsHotLink?: string | null; - mapCenter: [number, number]; - mapZoom: number; - outlineWeight: number | ''; - isDisplayWarning: boolean; - showAllShapes: boolean; - selectedLayer?: VectorLayer; - selectedJoinField?: FileLayerField; - wms: WMSOptions; -} diff --git a/src/legacy/core_plugins/region_map/public/util.ts b/src/legacy/core_plugins/region_map/public/util.ts index 24c721da1f31a..b4e0dcd5f3510 100644 --- a/src/legacy/core_plugins/region_map/public/util.ts +++ b/src/legacy/core_plugins/region_map/public/util.ts @@ -18,8 +18,7 @@ */ import { FileLayer, VectorLayer } from '../../../../plugins/maps_legacy/public'; -// TODO: reference to TILE_MAP plugin should be removed -import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin'; +import { ORIGIN } from '../../../../plugins/maps_legacy/public'; export const mapToLayerWithId = (prefix: string, layer: FileLayer): VectorLayer => ({ ...layer, diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index e1966a9e8b266..3348096c0e2f1 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -18,6 +18,7 @@ */ import { createReadStream } from 'fs'; +import { resolve } from 'path'; import globby from 'globby'; import MultiStream from 'multistream'; @@ -40,6 +41,7 @@ export default kibana => { }, uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), async __bundleProvider__(kbnServer) { const modules = new Set(); diff --git a/src/legacy/core_plugins/tests_bundle/public/index.scss b/src/legacy/core_plugins/tests_bundle/public/index.scss new file mode 100644 index 0000000000000..8020cef8d8492 --- /dev/null +++ b/src/legacy/core_plugins/tests_bundle/public/index.scss @@ -0,0 +1,6 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +// This file pulls some styles of NP plugins into the legacy test stylesheet +// so they are available for karma browser tests. +@import '../../../../plugins/vis_type_vislib/public/index'; +@import '../../../../plugins/visualizations/public/index'; diff --git a/src/legacy/core_plugins/tile_map/common/origin.ts b/src/legacy/core_plugins/tile_map/common/origin.ts deleted file mode 100644 index 7fcf1c659bdf3..0000000000000 --- a/src/legacy/core_plugins/tile_map/common/origin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export enum ORIGIN { - EMS = 'elastic_maps_service', - KIBANA_YML = 'self_hosted', -} diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 3904c43707906..bce2e157ebbc8 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -53,6 +53,7 @@ import { import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; function mockRawData() { const stack = [dummyESResponse]; @@ -114,15 +115,26 @@ describe('CoordinateMapsVisualizationTest', function() { return 'not found'; } }); + + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: {}, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; const serviceSettings = new ServiceSettings(); + const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); const uiSettings = $injector.get('config'); dependencies = { - serviceSettings, - uiSettings, - $injector, - getPrecision, getZoomPrecision, + getPrecision, + BaseMapsVisualization, + uiSettings, + serviceSettings, }; visType = new BaseVisType(createTileMapTypeDefinition(dependencies)); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js index fc029d6bccb6e..bdf9cd806eb8b 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js @@ -24,7 +24,8 @@ import scaledCircleMarkersPng from './scaledCircleMarkers.png'; // import shadedCircleMarkersPng from './shadedCircleMarkers.png'; import { ImageComparator } from 'test_utils/image_comparator'; import GeoHashSampleData from './dummy_es_response.json'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../../../plugins/maps_legacy/public/map/kibana_map'; describe('geohash_layer', function() { let domNode; diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index 9ca42fe3e4074..1efb0b2f884f8 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -28,9 +28,7 @@ import { SelectOption, SwitchOption, } from '../../../../../plugins/charts/public'; -import { WmsOptions } from './wms_options'; -import { TileMapVisParams } from '../types'; -import { MapTypes } from '../map_types'; +import { WmsOptions, TileMapVisParams, MapTypes } from '../../../../../plugins/maps_legacy/public'; export type TileMapOptionsProps = VisOptionsProps; diff --git a/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html b/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html deleted file mode 100644 index 9df5b94a21eda..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -
{{detail.label}}{{detail.value}}
diff --git a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js b/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js deleted file mode 100644 index eec90e512b462..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import { i18n } from '@kbn/i18n'; - -import template from './_tooltip.html'; - -export function TileMapTooltipFormatterProvider($injector) { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(template); - - $compile($el)($tooltipScope); - - return function tooltipFormatter(metricTitle, metricFormat, feature) { - if (!feature) { - return ''; - } - - $tooltipScope.details = [ - { - label: metricTitle, - value: metricFormat(feature.properties.value), - }, - { - label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { - defaultMessage: 'Latitude', - }), - value: feature.geometry.coordinates[1], - }, - { - label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { - defaultMessage: 'Longitude', - }), - value: feature.geometry.coordinates[0], - }, - ]; - - $tooltipScope.$apply(); - - return $el.html(); - }; -} diff --git a/src/legacy/core_plugins/tile_map/public/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/geohash_layer.js index b9acf1a15208f..f0261483d302d 100644 --- a/src/legacy/core_plugins/tile_map/public/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/geohash_layer.js @@ -20,12 +20,11 @@ import L from 'leaflet'; import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer } from '../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer, MapTypes } from '../../../../plugins/maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; import { GeohashGridMarkers } from './markers/geohash_grid'; -import { MapTypes } from './map_types'; export class GeohashLayer extends KibanaMapLayer { constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap) { diff --git a/src/legacy/core_plugins/tile_map/public/legacy.ts b/src/legacy/core_plugins/tile_map/public/legacy.ts index 741e118750f32..dd8d4c6e9311e 100644 --- a/src/legacy/core_plugins/tile_map/public/legacy.ts +++ b/src/legacy/core_plugins/tile_map/public/legacy.ts @@ -21,17 +21,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { TileMapPluginSetupDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, mapsLegacy: npSetup.plugins.mapsLegacy, - - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index 2b97407b17b38..aa1460a7e2890 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -25,22 +25,21 @@ import { } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; +// TODO: Determine why visualizations don't populate without this +import 'angular-sanitize'; // @ts-ignore import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; +import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; /** @private */ -interface TileMapVisualizationDependencies extends LegacyDependenciesPluginSetup { - serviceSettings: IServiceSettings; +interface TileMapVisualizationDependencies { uiSettings: IUiSettingsClient; getZoomPrecision: any; getPrecision: any; - notificationService: any; + BaseMapsVisualization: any; } /** @internal */ @@ -48,7 +47,6 @@ export interface TileMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; mapsLegacy: MapsLegacyPluginSetup; - __LEGACY: LegacyDependenciesPlugin; } /** @internal */ @@ -61,16 +59,14 @@ export class TileMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, mapsLegacy, __LEGACY }: TileMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy }: TileMapPluginSetupDependencies ) { - const { getZoomPrecision, getPrecision, serviceSettings } = mapsLegacy; + const { getZoomPrecision, getPrecision } = mapsLegacy; const visualizationDependencies: Readonly = { - serviceSettings, getZoomPrecision, getPrecision, - notificationService: core.notifications.toasts, + BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), uiSettings: core.uiSettings, - ...(await __LEGACY.setup()), }; expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); diff --git a/src/legacy/core_plugins/tile_map/public/shim/index.ts b/src/legacy/core_plugins/tile_map/public/shim/index.ts deleted file mode 100644 index cfc7b62ff4f86..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/shim/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './legacy_dependencies_plugin'; diff --git a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts deleted file mode 100644 index 5296e98b09efe..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import chrome from 'ui/chrome'; -import { CoreStart, Plugin } from 'kibana/public'; -// TODO: Determine why visualizations don't populate without this -import 'angular-sanitize'; - -/** @internal */ -export interface LegacyDependenciesPluginSetup { - $injector: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup() { - const $injector = await chrome.dangerouslyGetActiveInjector(); - - return { - $injector, - } as LegacyDependenciesPluginSetup; - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index ae3a839b600e9..ca6a586d22008 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -19,11 +19,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; +import { convertToGeoJson, MapTypes } from '../../../../plugins/maps_legacy/public'; import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; -import { MapTypes } from './map_types'; import { supportsCssFilters } from './css_filters'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index fdce8bc51fe86..6a7bda5e18883 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -19,30 +19,24 @@ import { get } from 'lodash'; import { GeohashLayer } from './geohash_layer'; -import { BaseMapsVisualizationProvider } from './base_maps_visualization'; -import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter'; import { npStart } from 'ui/new_platform'; import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -import { scaleBounds, geoContains } from '../../../../plugins/maps_legacy/public'; - -export const createTileMapVisualization = ({ - serviceSettings, - $injector, - getZoomPrecision, - getPrecision, - notificationService, -}) => { - const BaseMapsVisualization = new BaseMapsVisualizationProvider( - serviceSettings, - notificationService - ); - const tooltipFormatter = new TileMapTooltipFormatterProvider($injector); +import { + scaleBounds, + geoContains, + mapTooltipProvider, +} from '../../../../plugins/maps_legacy/public'; +import { tooltipFormatter } from './tooltip_formatter'; + +export const createTileMapVisualization = dependencies => { + const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies; return class CoordinateMapsVisualization extends BaseMapsVisualization { constructor(element, vis) { super(element, vis); this._geohashLayer = null; + this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter); } updateGeohashAgg = () => { @@ -190,18 +184,15 @@ export const createTileMapVisualization = ({ const metricDimension = this._params.dimensions.metric; const metricLabel = metricDimension ? metricDimension.label : ''; const metricFormat = getFormat(metricDimension && metricDimension.format); - const boundTooltipFormatter = tooltipFormatter.bind( - null, - metricLabel, - metricFormat.getConverterFor('text') - ); return { label: metricLabel, valueFormatter: this._geoJsonFeatureCollectionAndMeta ? metricFormat.getConverterFor('text') : null, - tooltipFormatter: this._geoJsonFeatureCollectionAndMeta ? boundTooltipFormatter : null, + tooltipFormatter: this._geoJsonFeatureCollectionAndMeta + ? this._tooltipFormatter.bind(null, metricLabel, metricFormat.getConverterFor('text')) + : null, mapType: newParams.mapType, isFilteredByCollar: this._isFilteredByCollar(), colorRamp: newParams.colorSchema, diff --git a/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js b/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js new file mode 100644 index 0000000000000..1c87d4dcca2b5 --- /dev/null +++ b/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +export function tooltipFormatter(metricTitle, metricFormat, feature) { + if (!feature) { + return ''; + } + + return [ + { + label: metricTitle, + value: metricFormat(feature.properties.value), + }, + { + label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { + defaultMessage: 'Latitude', + }), + value: feature.geometry.coordinates[1], + }, + { + label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { + defaultMessage: 'Longitude', + }), + value: feature.geometry.coordinates[0], + }, + ]; +} diff --git a/src/legacy/core_plugins/tile_map/public/types.ts b/src/legacy/core_plugins/tile_map/public/types.ts deleted file mode 100644 index e1b4c27319123..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { TmsLayer } from '../../../../plugins/maps_legacy/public'; -import { MapTypes } from './map_types'; - -export interface WMSOptions { - selectedTmsLayer?: TmsLayer; - enabled: boolean; - url?: string; - options: { - version?: string; - layers?: string; - format: string; - transparent: boolean; - attribution?: string; - styles?: string; - }; -} - -export interface TileMapVisParams { - colorSchema: string; - mapType: MapTypes; - isDesaturated: boolean; - addTooltip: boolean; - heatClusterSize: number; - legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft'; - mapZoom: number; - mapCenter: [number, number]; - wms: WMSOptions; -} diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index 7f5c7d4664af8..80ffa440e7285 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -18,6 +18,8 @@ */ import _ from 'lodash'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18n } from '@kbn/i18n'; diff --git a/src/legacy/core_plugins/vis_type_vislib/index.ts b/src/legacy/core_plugins/vis_type_vislib/index.ts deleted file mode 100644 index da9476285a9b2..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; - -const visTypeVislibPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vislib', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default visTypeVislibPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vislib/package.json b/src/legacy/core_plugins/vis_type_vislib/package.json deleted file mode 100644 index e30a9e2b35834..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "vis_type_vislib", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx deleted file mode 100644 index 3fca9dc8adc08..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { SwitchOption, TextInputOption } from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; - -function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { - return ( - - -

- -

-
- - - - setGaugeValue('labels', { ...stateParams.gauge.labels, [paramName]: value }) - } - /> - - - - setGaugeValue('style', { ...stateParams.gauge.style, [paramName]: value }) - } - /> - - -
- ); -} - -export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx deleted file mode 100644 index dc207ad89286f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { useCallback, useEffect, useState } from 'react'; - -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { - BasicOptions, - ColorRanges, - ColorSchemaOptions, - NumberInputOption, - SelectOption, - SwitchOption, - SetColorSchemaOptionsValue, - SetColorRangeValue, -} from '../../../../../../../plugins/charts/public'; -import { HeatmapVisParams } from '../../../heatmap'; -import { ValueAxis } from '../../../types'; -import { LabelsPanel } from './labels_panel'; - -function HeatmapOptions(props: VisOptionsProps) { - const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; - const [valueAxis] = stateParams.valueAxes; - const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; - const [isColorRangesValid, setIsColorRangesValid] = useState(false); - - const setValueAxisScale = useCallback( - (paramName: T, value: ValueAxis['scale'][T]) => - setValue('valueAxes', [ - { - ...valueAxis, - scale: { - ...valueAxis.scale, - [paramName]: value, - }, - }, - ]), - [valueAxis, setValue] - ); - - useEffect(() => { - setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid); - }, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]); - - return ( - <> - - -

- -

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

- -

-
- - - - - - - - - - - - - - - - - {stateParams.setColorRange && ( - - )} -
- - - - - - ); -} - -export { HeatmapOptions }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx deleted file mode 100644 index 3d1629740df2c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { useCallback } from 'react'; - -import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValueAxis } from '../../../types'; -import { HeatmapVisParams } from '../../../heatmap'; -import { SwitchOption } from '../../../../../../../plugins/charts/public'; - -const VERTICAL_ROTATION = 270; - -interface LabelsPanelProps { - valueAxis: ValueAxis; - setValue: VisOptionsProps['setValue']; -} - -function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { - const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION; - - const setValueAxisLabels = useCallback( - (paramName: T, value: ValueAxis['labels'][T]) => - setValue('valueAxes', [ - { - ...valueAxis, - labels: { - ...valueAxis.labels, - [paramName]: value, - }, - }, - ]), - [valueAxis, setValue] - ); - - const setRotateLabels = useCallback( - (paramName: 'rotate', value: boolean) => - setValueAxisLabels(paramName, value ? VERTICAL_ROTATION : 0), - [setValueAxisLabels] - ); - - const setColor = useCallback(value => setValueAxisLabels('color', value), [setValueAxisLabels]); - - return ( - - -

- -

-
- - - - - - - - - - - -
- ); -} - -export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/index.ts deleted file mode 100644 index 4d7091ffb204b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializerContext } from '../../../../core/public'; -import { VisTypeVislibPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} - -export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts deleted file mode 100644 index 579caa1cb88f6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from 'kibana/public'; - -import { plugin } from '.'; -import { - VisTypeVislibPluginSetupDependencies, - VisTypeVislibPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, - visTypeXy: npSetup.plugins.visTypeXy, -}; - -const startPlugins: Readonly = { - data: npStart.plugins.data, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts deleted file mode 100644 index c04ffa506eb04..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { search } from '../../../../plugins/data/public'; -export const { tabifyAggResponse, tabifyGetColumns } = search; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts deleted file mode 100644 index 26800f8a1620e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; - -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; -import { createPieVisFn } from './pie_fn'; -import { - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, -} from './vis_type_vislib_vis_types'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService, setDataActions } from './services'; - -export interface VisTypeVislibDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface VisTypeVislibPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; - visTypeXy?: VisTypeXyPluginSetup; -} - -/** @internal */ -export interface VisTypeVislibPluginStartDependencies { - data: DataPublicPluginStart; -} - -type VisTypeVislibCoreSetup = CoreSetup; - -/** @internal */ -export class VisTypeVislibPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: VisTypeVislibCoreSetup, - { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies - ) { - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - const vislibTypes = [ - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, - ]; - const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - - // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy) { - const convertedTypes: any[] = []; - const convertedFns: any[] = []; - - // Register legacy vislib types that have been converted - convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach(vis => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); - } - - // Register non-converted types - vislibFns.forEach(expressions.registerFunction); - vislibTypes.forEach(vis => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); - } - - public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { - setFormatService(data.fieldFormats); - setDataActions(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 0d6b1b5e8de58..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/services.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getDataActions, setDataActions] = createGetterSetter< - DataPublicPluginStart['actions'] ->('vislib data.actions'); - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('vislib data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/types.ts b/src/legacy/core_plugins/vis_type_vislib/public/types.ts deleted file mode 100644 index 25c6ae5439fe8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/types.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { TimeMarker } from './vislib/visualizations/time_marker'; -import { - Positions, - ChartModes, - ChartTypes, - AxisModes, - AxisTypes, - InterpolationModes, - ScaleTypes, - ThresholdLineStyles, -} from './utils/collections'; -import { Labels, Style } from '../../../../plugins/charts/public'; - -export interface CommonVislibParams { - addTooltip: boolean; - legendPosition: Positions; -} - -export interface Scale { - boundsMargin?: number | ''; - defaultYExtents?: boolean; - max?: number | null; - min?: number | null; - mode?: AxisModes; - setYExtents?: boolean; - type: ScaleTypes; -} - -interface ThresholdLine { - show: boolean; - value: number | null; - width: number | null; - style: ThresholdLineStyles; - color: string; -} - -export interface Axis { - id: string; - labels: Labels; - position: Positions; - scale: Scale; - show: boolean; - style: Style; - title: { text: string }; - type: AxisTypes; -} - -export interface ValueAxis extends Axis { - name: string; -} - -export interface SeriesParam { - data: { label: string; id: string }; - drawLinesBetweenPoints: boolean; - interpolate: InterpolationModes; - lineWidth?: number; - mode: ChartModes; - show: boolean; - showCircles: boolean; - type: ChartTypes; - valueAxis: string; -} - -export interface BasicVislibParams extends CommonVislibParams { - addTimeMarker: boolean; - categoryAxes: Axis[]; - orderBucketsBySum?: boolean; - labels: Labels; - thresholdLine: ThresholdLine; - valueAxes: ValueAxis[]; - grid: { - categoryLines: boolean; - valueAxis?: string; - }; - seriesParams: SeriesParam[]; - times: TimeMarker[]; -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js deleted file mode 100644 index a3aabcb90be62..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { pointSeriesTooltipFormatter } from '../../components/tooltip'; - -describe('tooltipFormatter', function() { - const tooltipFormatter = pointSeriesTooltipFormatter(); - - function cell($row, i) { - return $row - .eq(i) - .text() - .trim(); - } - - const baseEvent = { - data: { - xAxisLabel: 'inner', - xAxisFormatter: _.identity, - yAxisLabel: 'middle', - yAxisFormatter: _.identity, - zAxisLabel: 'top', - zAxisFormatter: _.identity, - series: [ - { - rawId: '1', - label: 'middle', - zLabel: 'top', - yAxisFormatter: _.identity, - zAxisFormatter: _.identity, - }, - ], - }, - datum: { - x: 3, - y: 2, - z: 1, - extraMetrics: [], - seriesId: '1', - }, - }; - - it('returns html based on the mouse event', function() { - const event = _.cloneDeep(baseEvent); - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - - const $row1 = $rows.eq(0).find('td'); - expect(cell($row1, 0)).to.be('inner'); - expect(cell($row1, 1)).to.be('3'); - - const $row2 = $rows.eq(1).find('td'); - expect(cell($row2, 0)).to.be('middle'); - expect(cell($row2, 1)).to.be('2'); - - const $row3 = $rows.eq(2).find('td'); - expect(cell($row3, 0)).to.be('top'); - expect(cell($row3, 1)).to.be('1'); - }); - - it('renders correctly on missing extraMetrics in datum', function() { - const event = _.cloneDeep(baseEvent); - delete event.datum.extraMetrics; - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js deleted file mode 100644 index db99b881a6e38..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { labels } from '../../components/labels/labels'; -import { dataArray } from '../../components/labels/data_array'; -import { uniqLabels } from '../../components/labels/uniq_labels'; -import { flattenSeries as getSeries } from '../../components/labels/flatten_series'; - -let seriesLabels; -let rowsLabels; -let seriesArr; -let rowsArr; - -const seriesData = { - label: '', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], -}; - -const rowsData = { - rows: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -const columnsData = { - columns: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -describe('Vislib Labels Module Test Suite', function() { - let uniqSeriesLabels; - describe('Labels (main)', function() { - beforeEach(() => { - seriesLabels = labels(seriesData); - rowsLabels = labels(rowsData); - seriesArr = Array.isArray(seriesLabels); - rowsArr = Array.isArray(rowsLabels); - uniqSeriesLabels = _.chain(rowsData.rows) - .pluck('series') - .flattenDeep() - .pluck('label') - .uniq() - .value(); - }); - - it('should be a function', function() { - expect(typeof labels).to.be('function'); - }); - - it('should return an array if input is data.series', function() { - expect(seriesArr).to.be(true); - }); - - it('should return an array if input is data.rows', function() { - expect(rowsArr).to.be(true); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - labels('string not object'); - }).to.throwError(); - }); - - it('should return unique label values', function() { - expect(rowsLabels[0]).to.equal(uniqSeriesLabels[0]); - expect(rowsLabels[1]).to.equal(uniqSeriesLabels[1]); - expect(rowsLabels[2]).to.equal(uniqSeriesLabels[2]); - }); - }); - - describe('Data array', function() { - const childrenObject = { - children: [], - }; - const seriesObject = { - series: [], - }; - const rowsObject = { - rows: [], - }; - const columnsObject = { - columns: [], - }; - const string = 'string'; - const number = 23; - const boolean = false; - const emptyArray = []; - const nullValue = null; - let notAValue; - let testSeries; - let testRows; - - beforeEach(() => { - seriesLabels = dataArray(seriesData); - rowsLabels = dataArray(rowsData); - testSeries = Array.isArray(seriesLabels); - testRows = Array.isArray(rowsLabels); - }); - - it('should throw an error if the input is not an object', function() { - expect(function() { - dataArray(string); - }).to.throwError(); - - expect(function() { - dataArray(number); - }).to.throwError(); - - expect(function() { - dataArray(boolean); - }).to.throwError(); - - expect(function() { - dataArray(emptyArray); - }).to.throwError(); - - expect(function() { - dataArray(nullValue); - }).to.throwError(); - - expect(function() { - dataArray(notAValue); - }).to.throwError(); - }); - - it( - 'should throw an error if property series, rows, or ' + 'columns is not present', - function() { - expect(function() { - dataArray(childrenObject); - }).to.throwError(); - } - ); - - it( - 'should not throw an error if object has property series, rows, or ' + 'columns', - function() { - expect(function() { - dataArray(seriesObject); - }).to.not.throwError(); - - expect(function() { - dataArray(rowsObject); - }).to.not.throwError(); - - expect(function() { - dataArray(columnsObject); - }).to.not.throwError(); - } - ); - - it('should be a function', function() { - expect(typeof dataArray).to.equal('function'); - }); - - it('should return an array of objects if input is data.series', function() { - expect(testSeries).to.equal(true); - }); - - it('should return an array of objects if input is data.rows', function() { - expect(testRows).to.equal(true); - }); - - it('should return an array of same length as input data.series', function() { - expect(seriesLabels.length).to.equal(seriesData.series.length); - }); - - it('should return an array of same length as input data.rows', function() { - expect(rowsLabels.length).to.equal(rowsData.rows.length); - }); - - it('should return an array of objects with obj.labels and obj.values', function() { - expect(seriesLabels[0].label).to.equal('100'); - expect(seriesLabels[0].values[0].x).to.equal(0); - expect(seriesLabels[0].values[0].y).to.equal(1); - }); - }); - - describe('Unique labels', function() { - const arrObj = [ - { label: 'a' }, - { label: 'b' }, - { label: 'b' }, - { label: 'c' }, - { label: 'c' }, - { label: 'd' }, - { label: 'f' }, - ]; - const string = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const emptyObject = {}; - const emptyArray = []; - let notAValue; - let uniq; - let testArr; - - beforeEach(() => { - uniq = uniqLabels(arrObj, function(d) { - return d; - }); - testArr = Array.isArray(uniq); - }); - - it('should throw an error if input is not an array', function() { - expect(function() { - uniqLabels(string); - }).to.throwError(); - - expect(function() { - uniqLabels(number); - }).to.throwError(); - - expect(function() { - uniqLabels(boolean); - }).to.throwError(); - - expect(function() { - uniqLabels(nullValue); - }).to.throwError(); - - expect(function() { - uniqLabels(emptyObject); - }).to.throwError(); - - expect(function() { - uniqLabels(notAValue); - }).to.throwError(); - }); - - it('should not throw an error if the input is an array', function() { - expect(function() { - uniqLabels(emptyArray); - }).to.not.throwError(); - }); - - it('should be a function', function() { - expect(typeof uniqLabels).to.be('function'); - }); - - it('should return an array', function() { - expect(testArr).to.be(true); - }); - - it('should return array of 5 unique values', function() { - expect(uniq.length).to.be(5); - }); - }); - - describe('Get series', function() { - const string = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const rowsObject = { - rows: [], - }; - const columnsObject = { - columns: [], - }; - const emptyObject = {}; - const emptyArray = []; - let notAValue; - let columnsLabels; - let rowsLabels; - let columnsArr; - let rowsArr; - - beforeEach(() => { - columnsLabels = getSeries(columnsData); - rowsLabels = getSeries(rowsData); - columnsArr = Array.isArray(columnsLabels); - rowsArr = Array.isArray(rowsLabels); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - getSeries(string); - }).to.throwError(); - - expect(function() { - getSeries(number); - }).to.throwError(); - - expect(function() { - getSeries(boolean); - }).to.throwError(); - - expect(function() { - getSeries(nullValue); - }).to.throwError(); - - expect(function() { - getSeries(emptyArray); - }).to.throwError(); - - expect(function() { - getSeries(notAValue); - }).to.throwError(); - }); - - it('should throw an if property rows or columns is not set on the object', function() { - expect(function() { - getSeries(emptyObject); - }).to.throwError(); - }); - - it('should not throw an error if rows or columns set on object', function() { - expect(function() { - getSeries(rowsObject); - }).to.not.throwError(); - - expect(function() { - getSeries(columnsObject); - }).to.not.throwError(); - }); - - it('should be a function', function() { - expect(typeof getSeries).to.be('function'); - }); - - it('should return an array if input is data.columns', function() { - expect(columnsArr).to.be(true); - }); - - it('should return an array if input is data.rows', function() { - expect(rowsArr).to.be(true); - }); - - it('should return an array of the same length as as input data.columns', function() { - expect(columnsLabels.length).to.be(columnsData.columns.length); - }); - - it('should return an array of the same length as as input data.rows', function() { - expect(rowsLabels.length).to.be(rowsData.rows.length); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js deleted file mode 100644 index f1c80c9981020..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import $ from 'jquery'; -import _ from 'lodash'; -import sinon from 'sinon'; - -import { positionTooltip } from '../../components/tooltip/position_tooltip'; - -describe('Tooltip Positioning', function() { - const sandbox = sinon.createSandbox(); - const positions = ['north', 'south', 'east', 'west']; - const bounds = ['top', 'left', 'bottom', 'right', 'area']; - let $window; - let $chart; - let $tooltip; - let $sizer; - - function testEl(width, height, $children) { - const $el = $('
'); - - const size = { - width: _.random(width[0], width[1]), - height: _.random(height[0], height[1]), - }; - - $el - .css({ - width: size.width, - height: size.height, - visibility: 'hidden', - }) - .appendTo('body'); - - if ($children) { - $el.append($children); - } - - $el.testSize = size; - - return $el; - } - - beforeEach(function() { - $window = testEl( - [500, 1000], - [600, 800], - ($chart = testEl([600, 750], [350, 550], ($tooltip = testEl([50, 100], [35, 75])))) - ); - - $sizer = $tooltip.clone().appendTo($window); - }); - - afterEach(function() { - $window.remove(); - $window = $chart = $tooltip = $sizer = null; - positionTooltip.removeClone(); - sandbox.restore(); - }); - - function makeEvent(xPercent, yPercent) { - xPercent = xPercent || 0.5; - yPercent = yPercent || 0.5; - - const base = $chart.offset(); - - return { - clientX: base.left + $chart.testSize.width * xPercent, - clientY: base.top + $chart.testSize.height * yPercent, - }; - } - - describe('getTtSize()', function() { - it('should measure the outer-size of the tooltip using an un-obstructed clone', function() { - const w = sandbox.spy($.fn, 'outerWidth'); - const h = sandbox.spy($.fn, 'outerHeight'); - - positionTooltip.getTtSize($tooltip.html(), $sizer); - - [w, h].forEach(function(spy) { - expect(spy).to.have.property('callCount', 1); - const matchHtml = w.thisValues.filter(function($t) { - return !$t.is($tooltip) && $t.html() === $tooltip.html(); - }); - expect(matchHtml).to.have.length(1); - }); - }); - }); - - describe('getBasePosition()', function() { - it('calculates the offset values for the four positions', function() { - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - const pos = positionTooltip.getBasePosition(size, makeEvent()); - - positions.forEach(function(p) { - expect(pos).to.have.property(p); - }); - - expect(pos.north).to.be.lessThan(pos.south); - expect(pos.east).to.be.greaterThan(pos.west); - }); - }); - - describe('getBounds()', function() { - it('returns the offsets for the tlrb of the element', function() { - const cbounds = positionTooltip.getBounds($chart); - - bounds.forEach(function(b) { - expect(cbounds).to.have.property(b); - }); - - expect(cbounds.top).to.be.lessThan(cbounds.bottom); - expect(cbounds.left).to.be.lessThan(cbounds.right); - }); - }); - - describe('getOverflow()', function() { - it('determines how much the base placement overflows the containing bounds in each direction', function() { - // size the tooltip very small so it won't collide with the edges - $tooltip.css({ width: 15, height: 15 }); - $sizer.css({ width: 15, height: 15 }); - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - expect(size).to.have.property('width', 15); - expect(size).to.have.property('height', 15); - - // position the element based on a mouse that is in the middle of the chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); - - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - positions.forEach(function(p) { - expect(overflow).to.have.property(p); - - // all positions should be less than 0 because the tooltip is so much smaller than the chart - expect(overflow[p]).to.be.lessThan(0); - }); - }); - - it('identifies an overflow with a positive value in that direction', function() { - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - - // position the element based on a mouse that is in the bottom right hand corner of the chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.99, 0.99)); - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - - positions.forEach(function(p) { - expect(overflow).to.have.property(p); - - if (p === 'south' || p === 'east') { - expect(overflow[p]).to.be.greaterThan(0); - } else { - expect(overflow[p]).to.be.lessThan(0); - } - }); - }); - - it('identifies only right overflow when tooltip overflows both sides of inner container but outer contains tooltip', function() { - // Size $tooltip larger than chart - const largeWidth = $chart.width() + 10; - $tooltip.css({ width: largeWidth }); - $sizer.css({ width: largeWidth }); - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - expect(size).to.have.property('width', largeWidth); - - // $chart is flush with the $window on the left side - expect(positionTooltip.getBounds($chart).left).to.be(0); - - // Size $window large enough for tooltip on right side - $window.css({ width: $chart.width() * 3 }); - - // Position click event in center of $chart so $tooltip overflows both sides of chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); - - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - - // no overflow on left (east) - expect(overflow.east).to.be.lessThan(0); - // overflow on right (west) - expect(overflow.west).to.be.greaterThan(0); - }); - }); - - describe('positionTooltip() integration', function() { - it('returns nothing if the $chart or $tooltip are not passed in', function() { - expect(positionTooltip() === void 0).to.be(true); - expect(positionTooltip(null, null, null) === void 0).to.be(true); - expect(positionTooltip(null, $(), $()) === void 0).to.be(true); - }); - - function check(xPercent, yPercent /*, prev, directions... */) { - const directions = _.drop(arguments, 2); - const event = makeEvent(xPercent, yPercent); - const placement = positionTooltip({ - $window: $window, - $chart: $chart, - $sizer: $sizer, - event: event, - $el: $tooltip, - prev: _.isObject(directions[0]) ? directions.shift() : null, - }); - - expect(placement) - .to.have.property('top') - .and.property('left'); - - directions.forEach(function(dir) { - switch (dir) { - case 'top': - expect(placement.top).to.be.lessThan(event.clientY); - return; - case 'bottom': - expect(placement.top).to.be.greaterThan(event.clientY); - return; - case 'right': - expect(placement.left).to.be.greaterThan(event.clientX); - return; - case 'left': - expect(placement.left).to.be.lessThan(event.clientX); - return; - } - }); - - return placement; - } - - describe('calculates placement of the tooltip properly', function() { - it('mouse is in the middle', function() { - check(0.5, 0.5, 'bottom', 'right'); - }); - - it('mouse is in the top left', function() { - check(0.1, 0.1, 'bottom', 'right'); - }); - - it('mouse is in the top right', function() { - check(0.99, 0.1, 'bottom', 'left'); - }); - - it('mouse is in the bottom right', function() { - check(0.99, 0.99, 'top', 'left'); - }); - - it('mouse is in the bottom left', function() { - check(0.1, 0.99, 'top', 'right'); - }); - }); - - describe('maintain the direction of the tooltip on reposition', function() { - it('mouse moves from the top right to the middle', function() { - const pos = check(0.99, 0.1, 'bottom', 'left'); - check(0.5, 0.5, pos, 'bottom', 'left'); - }); - - it('mouse moves from the bottom left to the middle', function() { - const pos = check(0.1, 0.99, 'top', 'right'); - check(0.5, 0.5, pos, 'top', 'right'); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js deleted file mode 100644 index 734c6d003278f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import VislibProvider from '..'; - -describe('Vislib Index Test Suite', function() { - let vislib; - - beforeEach(() => { - vislib = new VislibProvider(); - }); - - it('should return an object', function() { - expect(_.isObject(vislib)).to.be(true); - }); - - it('should return a Vis function', function() { - expect(_.isFunction(vislib.Vis)).to.be(true); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js deleted file mode 100644 index bc4a4f9925513..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { Axis } from '../../../lib/axis'; -import { VisConfig } from '../../../lib/vis_config'; -import { getMockUiState } from '../fixtures/_vis_fixture'; - -describe('Vislib Axis Class Test Suite', function() { - let mockUiState; - let yAxis; - let el; - let fixture; - let seriesData; - - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734130000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - ], - }, - { - label: 'Count2', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734140000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - ], - }, - ], - xAxisFormatter: function(thing) { - return new Date(thing); - }, - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - mockUiState = getMockUiState(); - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); - - fixture = el.append('div').attr('class', 'x-axis-div'); - - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - $('.x-axis-div')[0], - () => undefined - ); - yAxis = new Axis(visConfig, { - type: 'value', - id: 'ValueAxis-1', - }); - - seriesData = data.series.map(series => { - return series.values; - }); - }); - - afterEach(function() { - fixture.remove(); - el.remove(); - }); - - describe('_stackNegAndPosVals Method', function() { - it('should correctly stack positive values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 8, - }, - { - x: 1408734090000, - y: 23, - y0: 23, - }, - { - x: 1408734120000, - y: 30, - y0: 30, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 28, - }, - ]; - const stackedData = yAxis._stackNegAndPosVals(seriesData); - expect(stackedData[1]).to.eql(expectedResult); - }); - - it('should correctly stack pos and neg values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 0, - }, - { - x: 1408734090000, - y: 23, - y0: 0, - }, - { - x: 1408734120000, - y: 30, - y0: 0, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 0, - }, - ]; - const dataClone = _.cloneDeep(seriesData); - dataClone[0].forEach(value => { - value.y = -value.y; - }); - const stackedData = yAxis._stackNegAndPosVals(dataClone); - expect(stackedData[1]).to.eql(expectedResult); - }); - - it('should correctly stack mixed pos and neg values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 8, - }, - { - x: 1408734090000, - y: 23, - y0: 0, - }, - { - x: 1408734120000, - y: 30, - y0: 30, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 28, - }, - ]; - const dataClone = _.cloneDeep(seriesData); - dataClone[0].forEach((value, i) => { - if (i % 2 === 1) value.y = -value.y; - }); - const stackedData = yAxis._stackNegAndPosVals(dataClone); - expect(stackedData[1]).to.eql(expectedResult); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js deleted file mode 100644 index fd25335dd2cd4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { AxisTitle } from '../../lib/axis/axis_title'; -import { AxisConfig } from '../../lib/axis/axis_config'; -import { VisConfig } from '../../lib/vis_config'; -import { Data } from '../../lib/data'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib AxisTitle Class Test Suite', function() { - let el; - let dataObj; - let xTitle; - let yTitle; - let visConfig; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper'); - - el.append('div') - .attr('class', 'visAxis__column--bottom') - .append('div') - .attr('class', 'axis-title y-axis-title') - .style('height', '20px') - .style('width', '20px'); - - el.append('div') - .attr('class', 'visAxis__column--left') - .append('div') - .attr('class', 'axis-title x-axis-title') - .style('height', '20px') - .style('width', '20px'); - - const uiState = getMockUiState(); - uiState.set('vis.colors', []); - dataObj = new Data(data, getMockUiState(), () => undefined); - visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - getMockUiState(), - el.node(), - () => undefined - ); - const xAxisConfig = new AxisConfig(visConfig, { - position: 'bottom', - title: { - text: dataObj.get('xAxisLabel'), - }, - }); - const yAxisConfig = new AxisConfig(visConfig, { - position: 'left', - title: { - text: dataObj.get('yAxisLabel'), - }, - }); - xTitle = new AxisTitle(xAxisConfig); - yTitle = new AxisTitle(yAxisConfig); - }); - - afterEach(function() { - el.remove(); - }); - - it('should not do anything if title.show is set to false', function() { - const xAxisConfig = new AxisConfig(visConfig, { - position: 'bottom', - show: false, - title: { - text: dataObj.get('xAxisLabel'), - }, - }); - xTitle = new AxisTitle(xAxisConfig); - xTitle.render(); - expect( - $(el.node()) - .find('.x-axis-title') - .find('svg').length - ).to.be(0); - }); - - describe('render Method', function() { - beforeEach(function() { - xTitle.render(); - yTitle.render(); - }); - - it('should append an svg to div', function() { - expect(el.select('.x-axis-title').selectAll('svg').length).to.be(1); - expect(el.select('.y-axis-title').selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect( - el - .select('.x-axis-title') - .selectAll('svg') - .select('g').length - ).to.be(1); - expect( - el - .select('.y-axis-title') - .selectAll('svg') - .select('g').length - ).to.be(1); - }); - - it('should append text', function() { - expect( - !!el - .select('.x-axis-title') - .selectAll('svg') - .selectAll('text') - ).to.be(true); - expect( - !!el - .select('.y-axis-title') - .selectAll('svg') - .selectAll('text') - ).to.be(true); - }); - }); - - describe('draw Method', function() { - it('should be a function', function() { - expect(_.isFunction(xTitle.draw())).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js deleted file mode 100644 index d4ec6f363a75b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { Data } from '../../lib/data'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -const seriesData = { - label: '', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], -}; - -const rowsData = { - rows: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -const colsData = { - columns: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -describe('Vislib Data Class Test Suite', function() { - let mockUiState; - - beforeEach(() => { - mockUiState = getMockUiState(); - }); - - describe('Data Class (main)', function() { - it('should be a function', function() { - expect(_.isFunction(Data)).to.be(true); - }); - - it('should return an object', function() { - const rowIn = new Data(rowsData, mockUiState, () => undefined); - expect(_.isObject(rowIn)).to.be(true); - }); - }); - - describe('_removeZeroSlices', function() { - let data; - const pieData = { - slices: { - children: [{ size: 30 }, { size: 20 }, { size: 0 }], - }, - }; - - beforeEach(function() { - data = new Data(pieData, mockUiState, () => undefined); - }); - - it('should remove zero values', function() { - const slices = data._removeZeroSlices(data.data.slices); - expect(slices.children.length).to.be(2); - }); - }); - - describe('Data.flatten', function() { - let serIn; - let serOut; - - beforeEach(function() { - serIn = new Data(seriesData, mockUiState, () => undefined); - serOut = serIn.flatten(); - }); - - it('should return an array of value objects from every series', function() { - expect(serOut.every(_.isObject)).to.be(true); - }); - - it('should return all points from every series', testLength(seriesData)); - it('should return all points from every series in the rows', testLength(rowsData)); - it('should return all points from every series in the columns', testLength(colsData)); - - function testLength(inputData) { - return function() { - const data = new Data(inputData, mockUiState, () => undefined); - const len = _.reduce( - data.chartData(), - function(sum, chart) { - return ( - sum + - chart.series.reduce(function(sum, series) { - return sum + series.values.length; - }, 0) - ); - }, - 0 - ); - - expect(data.flatten()).to.have.length(len); - }; - } - }); - - describe('geohashGrid methods', function() { - let data; - const geohashGridData = { - hits: 3954, - rows: [ - { - title: 'Top 5 _type: apache', - label: 'Top 5 _type: apache', - geoJson: { - type: 'FeatureCollection', - features: [], - properties: { - min: 2, - max: 331, - zoom: 3, - center: [47.517200697839414, -112.06054687499999], - }, - }, - }, - { - title: 'Top 5 _type: nginx', - label: 'Top 5 _type: nginx', - geoJson: { - type: 'FeatureCollection', - features: [], - properties: { - min: 1, - max: 88, - zoom: 3, - center: [47.517200697839414, -112.06054687499999], - }, - }, - }, - ], - }; - - beforeEach(function() { - data = new Data(geohashGridData, mockUiState, () => undefined); - }); - - describe('getVisData', function() { - it('should return the rows property', function() { - const visData = data.getVisData(); - expect(visData[0].title).to.eql(geohashGridData.rows[0].title); - }); - }); - - describe('getGeoExtents', function() { - it('should return the min and max geoJson properties', function() { - const minMax = data.getGeoExtents(); - expect(minMax.min).to.be(1); - expect(minMax.max).to.be(331); - }); - }); - }); - - describe('null value check', function() { - it('should return false', function() { - const data = new Data(rowsData, mockUiState, () => undefined); - expect(data.hasNullValues()).to.be(false); - }); - - it('should return true', function() { - const nullRowData = { rows: rowsData.rows.slice(0) }; - nullRowData.rows.push({ - label: 'e', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: null }, - { x: 2, y: 3 }, - ], - }, - ], - }); - - const data = new Data(nullRowData, mockUiState, () => undefined); - expect(data.hasNullValues()).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js deleted file mode 100644 index 8fe9ac24db77b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import mockDispatchDataD3 from './fixtures/dispatch_bar_chart_d3.json'; -import { Dispatch } from '../../lib/dispatch'; -import mockdataPoint from './fixtures/dispatch_bar_chart_data_point.json'; -import mockConfigPercentage from './fixtures/dispatch_bar_chart_config_percentage.json'; -import mockConfigNormal from './fixtures/dispatch_bar_chart_config_normal.json'; - -jest.mock('ui/new_platform'); -jest.mock('d3', () => ({ - event: { - target: { - nearestViewportElement: { - __data__: mockDispatchDataD3, - }, - }, - }, -})); -jest.mock('../../../legacy_imports.ts', () => ({ - ...jest.requireActual('../../../legacy_imports.ts'), - chrome: { - getUiSettingsClient: () => ({ - get: () => '', - }), - addBasePath: () => {}, - }, -})); - -function getHandlerMock(config = {}, data = {}) { - return { - visConfig: { get: (id, fallback) => config[id] || fallback }, - data, - }; -} - -describe('Vislib event responses dispatcher', () => { - test('return data for a vertical bars popover in percentage mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigPercentage); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeTruthy(); - }); - - test('return data for a vertical bars popover in normal mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigNormal); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeFalsy(); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js deleted file mode 100644 index 4523e70ccbb4c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { ErrorHandler } from '../../lib/_error_handler'; - -describe('Vislib ErrorHandler Test Suite', function() { - let errorHandler; - - beforeEach(() => { - errorHandler = new ErrorHandler(); - }); - - describe('validateWidthandHeight Method', function() { - it('should throw an error when width and/or height is 0', function() { - expect(function() { - errorHandler.validateWidthandHeight(0, 200); - }).to.throwError(); - expect(function() { - errorHandler.validateWidthandHeight(200, 0); - }).to.throwError(); - }); - - it('should throw an error when width and/or height is NaN', function() { - expect(function() { - errorHandler.validateWidthandHeight(null, 200); - }).to.throwError(); - expect(function() { - errorHandler.validateWidthandHeight(200, null); - }).to.throwError(); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js deleted file mode 100644 index b5b14c279b40e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js +++ /dev/null @@ -1,300 +0,0 @@ -import moment from 'moment'; - -export default { - 'columns': [ - { - 'label': '200: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826600000, - 'y': 4 - }, - { - 'x': 1415826630000, - 'y': 8 - }, - { - 'x': 1415826660000, - 'y': 7 - }, - { - 'x': 1415826690000, - 'y': 5 - }, - { - 'x': 1415826720000, - 'y': 5 - }, - { - 'x': 1415826750000, - 'y': 4 - }, - { - 'x': 1415826780000, - 'y': 10 - }, - { - 'x': 1415826810000, - 'y': 7 - }, - { - 'x': 1415826840000, - 'y': 9 - }, - { - 'x': 1415826870000, - 'y': 8 - }, - { - 'x': 1415826900000, - 'y': 9 - }, - { - 'x': 1415826930000, - 'y': 8 - }, - { - 'x': 1415826960000, - 'y': 3 - }, - { - 'x': 1415826990000, - 'y': 9 - }, - { - 'x': 1415827020000, - 'y': 6 - }, - { - 'x': 1415827050000, - 'y': 8 - }, - { - 'x': 1415827080000, - 'y': 7 - }, - { - 'x': 1415827110000, - 'y': 4 - }, - { - 'x': 1415827140000, - 'y': 6 - }, - { - 'x': 1415827170000, - 'y': 10 - }, - { - 'x': 1415827200000, - 'y': 2 - }, - { - 'x': 1415827230000, - 'y': 8 - }, - { - 'x': 1415827260000, - 'y': 5 - }, - { - 'x': 1415827290000, - 'y': 6 - }, - { - 'x': 1415827320000, - 'y': 6 - }, - { - 'x': 1415827350000, - 'y': 10 - }, - { - 'x': 1415827380000, - 'y': 6 - }, - { - 'x': 1415827410000, - 'y': 6 - }, - { - 'x': 1415827440000, - 'y': 12 - }, - { - 'x': 1415827470000, - 'y': 9 - }, - { - 'x': 1415827500000, - 'y': 1 - } - ] - } - ] - }, - { - 'label': '503: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826630000, - 'y': 1 - }, - { - 'x': 1415826660000, - 'y': 1 - }, - { - 'x': 1415826720000, - 'y': 1 - }, - { - 'x': 1415826780000, - 'y': 1 - }, - { - 'x': 1415826900000, - 'y': 1 - }, - { - 'x': 1415827020000, - 'y': 1 - }, - { - 'x': 1415827080000, - 'y': 1 - }, - { - 'x': 1415827110000, - 'y': 2 - } - ] - } - ] - }, - { - 'label': '404: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826660000, - 'y': 1 - }, - { - 'x': 1415826720000, - 'y': 1 - }, - { - 'x': 1415826810000, - 'y': 1 - }, - { - 'x': 1415826960000, - 'y': 1 - }, - { - 'x': 1415827050000, - 'y': 1 - }, - { - 'x': 1415827260000, - 'y': 1 - }, - { - 'x': 1415827380000, - 'y': 1 - }, - { - 'x': 1415827410000, - 'y': 1 - } - ] - } - ] - } - ], - 'xAxisOrderedValues': [ - 1415826600000, - 1415826630000, - 1415826660000, - 1415826690000, - 1415826720000, - 1415826750000, - 1415826780000, - 1415826810000, - 1415826840000, - 1415826870000, - 1415826900000, - 1415826930000, - 1415826960000, - 1415826990000, - 1415827020000, - 1415827050000, - 1415827080000, - 1415827110000, - 1415827140000, - 1415827170000, - 1415827200000, - 1415827230000, - 1415827260000, - 1415827290000, - 1415827320000, - 1415827350000, - 1415827380000, - 1415827410000, - 1415827440000, - 1415827470000, - 1415827500000, - ], - 'hits': 225 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js deleted file mode 100644 index 98609d8ffbcd3..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js +++ /dev/null @@ -1,1678 +0,0 @@ -import moment from 'moment'; - -export default { - 'rows': [ - { - 'label': '0.0-1000.0: bytes', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826260456, - 'max': 1415827160456 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 3, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 2 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': '1000.0-2000.0: bytes', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826260457, - 'max': 1415827160457 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 2, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - } - ] - } - ], - 'xAxisOrderedValues': [ - 1415826240000, - 1415826270000, - 1415826300000, - 1415826330000, - 1415826360000, - 1415826390000, - 1415826420000, - 1415826450000, - 1415826480000, - 1415826510000, - 1415826540000, - 1415826570000, - 1415826600000, - 1415826630000, - 1415826660000, - 1415826690000, - 1415826720000, - 1415826750000, - 1415826780000, - 1415826810000, - 1415826840000, - 1415826870000, - 1415826900000, - 1415826930000, - 1415826960000, - 1415826990000, - 1415827020000, - 1415827050000, - 1415827080000, - 1415827110000, - 1415827140000, - ], - 'hits': 236 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js deleted file mode 100644 index 4ca631c7fc497..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js +++ /dev/null @@ -1,123 +0,0 @@ -import moment from 'moment'; - -export const rowsSeriesWithHoles = { - rows: [ - { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761510000, - 'y': 22 - }, - { - 'x': 1411761540000, - 'y': 17 - }, - { - 'x': 1411761840000, - 'y': 20 - }, - { - 'x': 1411761870000, - 'y': 20 - }, - { - 'x': 1411761900000, - 'y': 21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': 13 - }, - { - 'x': 1411762020000, - 'y': 14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': 17 - }, - { - 'x': 1411762110000, - 'y': 14 - }, - { - 'x': 1411762140000, - 'y': 22 - }, - { - 'x': 1411762170000, - 'y': 14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762320000, - 'y': 15 - }, - { - 'x': 1411762350000, - 'y': 4 - } - ] - } - ], - 'hits': 533, - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 1411761450000, - 1411761510000, - 1411761540000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762320000, - 1411762350000, - ], -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js deleted file mode 100644 index 13e2ab7b7fb1a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761480000, - 'y': 18 - }, - { - 'x': 1411761510000, - 'y': 22 - }, - { - 'x': 1411761540000, - 'y': 17 - }, - { - 'x': 1411761570000, - 'y': 17 - }, - { - 'x': 1411761600000, - 'y': 21 - }, - { - 'x': 1411761630000, - 'y': 16 - }, - { - 'x': 1411761660000, - 'y': 17 - }, - { - 'x': 1411761690000, - 'y': 15 - }, - { - 'x': 1411761720000, - 'y': 19 - }, - { - 'x': 1411761750000, - 'y': 11 - }, - { - 'x': 1411761780000, - 'y': 13 - }, - { - 'x': 1411761810000, - 'y': 24 - }, - { - 'x': 1411761840000, - 'y': 20 - }, - { - 'x': 1411761870000, - 'y': 20 - }, - { - 'x': 1411761900000, - 'y': 21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': 13 - }, - { - 'x': 1411762020000, - 'y': 14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': 17 - }, - { - 'x': 1411762110000, - 'y': 14 - }, - { - 'x': 1411762140000, - 'y': 22 - }, - { - 'x': 1411762170000, - 'y': 14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762230000, - 'y': 22 - }, - { - 'x': 1411762260000, - 'y': 17 - }, - { - 'x': 1411762290000, - 'y': 8 - }, - { - 'x': 1411762320000, - 'y': 15 - }, - { - 'x': 1411762350000, - 'y': 4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js deleted file mode 100644 index 6b7c574ab5551..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js +++ /dev/null @@ -1,89 +0,0 @@ -import moment from 'moment'; - -export const seriesMonthlyInterval = { - 'label': '', - 'xAxisLabel': '@timestamp per month', - 'ordered': { - 'date': true, - 'min': 1451631600000, - 'max': 1483254000000, - 'interval': 2678000000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1451631600000, - 'y': 10220 - }, - { - 'x': 1454310000000, - 'y': 9997, - }, - { - 'x': 1456815600000, - 'y': 10792, - }, - { - 'x': 1459490400000, - 'y': 10262 - }, - { - 'x': 1462082400000, - 'y': 10080 - }, - { - 'x': 1464760800000, - 'y': 11161 - }, - { - 'x': 1467352800000, - 'y': 9933 - }, - { - 'x': 1470031200000, - 'y': 10342 - }, - { - 'x': 1472709600000, - 'y': 10887 - }, - { - 'x': 1475301600000, - 'y': 9666 - }, - { - 'x': 1477980000000, - 'y': 9556 - }, - { - 'x': 1480575600000, - 'y': 11644 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1451631600000, - 1454310000000, - 1456815600000, - 1459490400000, - 1462082400000, - 1464760800000, - 1467352800000, - 1470031200000, - 1472709600000, - 1475301600000, - 1477980000000, - 1480575600000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js deleted file mode 100644 index ff5cd05b2f2d4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': -41 - }, - { - 'x': 1411761480000, - 'y': -18 - }, - { - 'x': 1411761510000, - 'y': -22 - }, - { - 'x': 1411761540000, - 'y': -17 - }, - { - 'x': 1411761570000, - 'y': -17 - }, - { - 'x': 1411761600000, - 'y': -21 - }, - { - 'x': 1411761630000, - 'y': -16 - }, - { - 'x': 1411761660000, - 'y': -17 - }, - { - 'x': 1411761690000, - 'y': -15 - }, - { - 'x': 1411761720000, - 'y': -19 - }, - { - 'x': 1411761750000, - 'y': -11 - }, - { - 'x': 1411761780000, - 'y': -13 - }, - { - 'x': 1411761810000, - 'y': -24 - }, - { - 'x': 1411761840000, - 'y': -20 - }, - { - 'x': 1411761870000, - 'y': -20 - }, - { - 'x': 1411761900000, - 'y': -21 - }, - { - 'x': 1411761930000, - 'y': -17 - }, - { - 'x': 1411761960000, - 'y': -20 - }, - { - 'x': 1411761990000, - 'y': -13 - }, - { - 'x': 1411762020000, - 'y': -14 - }, - { - 'x': 1411762050000, - 'y': -25 - }, - { - 'x': 1411762080000, - 'y': -17 - }, - { - 'x': 1411762110000, - 'y': -14 - }, - { - 'x': 1411762140000, - 'y': -22 - }, - { - 'x': 1411762170000, - 'y': -14 - }, - { - 'x': 1411762200000, - 'y': -19 - }, - { - 'x': 1411762230000, - 'y': -22 - }, - { - 'x': 1411762260000, - 'y': -17 - }, - { - 'x': 1411762290000, - 'y': -8 - }, - { - 'x': 1411762320000, - 'y': -15 - }, - { - 'x': 1411762350000, - 'y': -4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js deleted file mode 100644 index 06d9b31dc6b57..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761480000, - 'y': 18 - }, - { - 'x': 1411761510000, - 'y': -22 - }, - { - 'x': 1411761540000, - 'y': -17 - }, - { - 'x': 1411761570000, - 'y': -17 - }, - { - 'x': 1411761600000, - 'y': -21 - }, - { - 'x': 1411761630000, - 'y': -16 - }, - { - 'x': 1411761660000, - 'y': 17 - }, - { - 'x': 1411761690000, - 'y': 15 - }, - { - 'x': 1411761720000, - 'y': 19 - }, - { - 'x': 1411761750000, - 'y': 11 - }, - { - 'x': 1411761780000, - 'y': -13 - }, - { - 'x': 1411761810000, - 'y': -24 - }, - { - 'x': 1411761840000, - 'y': -20 - }, - { - 'x': 1411761870000, - 'y': -20 - }, - { - 'x': 1411761900000, - 'y': -21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': -13 - }, - { - 'x': 1411762020000, - 'y': -14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': -17 - }, - { - 'x': 1411762110000, - 'y': -14 - }, - { - 'x': 1411762140000, - 'y': -22 - }, - { - 'x': 1411762170000, - 'y': -14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762230000, - 'y': 22 - }, - { - 'x': 1411762260000, - 'y': 17 - }, - { - 'x': 1411762290000, - 'y': 8 - }, - { - 'x': 1411762320000, - 'y': -15 - }, - { - 'x': 1411762350000, - 'y': -4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js deleted file mode 100644 index 5208c7e996cd8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js +++ /dev/null @@ -1,1557 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 10 min', - 'ordered': { - 'date': true, - 'min': 1413544140087, - 'max': 1413587340087, - 'interval': 600000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'html', - 'values': [ - { - 'x': 1413543600000, - 'y': 140 - }, - { - 'x': 1413544200000, - 'y': 1388 - }, - { - 'x': 1413544800000, - 'y': 1308 - }, - { - 'x': 1413545400000, - 'y': 1356 - }, - { - 'x': 1413546000000, - 'y': 1314 - }, - { - 'x': 1413546600000, - 'y': 1343 - }, - { - 'x': 1413547200000, - 'y': 1353 - }, - { - 'x': 1413547800000, - 'y': 1353 - }, - { - 'x': 1413548400000, - 'y': 1334 - }, - { - 'x': 1413549000000, - 'y': 1433 - }, - { - 'x': 1413549600000, - 'y': 1331 - }, - { - 'x': 1413550200000, - 'y': 1349 - }, - { - 'x': 1413550800000, - 'y': 1323 - }, - { - 'x': 1413551400000, - 'y': 1203 - }, - { - 'x': 1413552000000, - 'y': 1231 - }, - { - 'x': 1413552600000, - 'y': 1227 - }, - { - 'x': 1413553200000, - 'y': 1187 - }, - { - 'x': 1413553800000, - 'y': 1119 - }, - { - 'x': 1413554400000, - 'y': 1159 - }, - { - 'x': 1413555000000, - 'y': 1117 - }, - { - 'x': 1413555600000, - 'y': 1152 - }, - { - 'x': 1413556200000, - 'y': 1057 - }, - { - 'x': 1413556800000, - 'y': 1009 - }, - { - 'x': 1413557400000, - 'y': 979 - }, - { - 'x': 1413558000000, - 'y': 975 - }, - { - 'x': 1413558600000, - 'y': 848 - }, - { - 'x': 1413559200000, - 'y': 873 - }, - { - 'x': 1413559800000, - 'y': 808 - }, - { - 'x': 1413560400000, - 'y': 784 - }, - { - 'x': 1413561000000, - 'y': 799 - }, - { - 'x': 1413561600000, - 'y': 684 - }, - { - 'x': 1413562200000, - 'y': 727 - }, - { - 'x': 1413562800000, - 'y': 621 - }, - { - 'x': 1413563400000, - 'y': 615 - }, - { - 'x': 1413564000000, - 'y': 569 - }, - { - 'x': 1413564600000, - 'y': 523 - }, - { - 'x': 1413565200000, - 'y': 474 - }, - { - 'x': 1413565800000, - 'y': 470 - }, - { - 'x': 1413566400000, - 'y': 466 - }, - { - 'x': 1413567000000, - 'y': 394 - }, - { - 'x': 1413567600000, - 'y': 404 - }, - { - 'x': 1413568200000, - 'y': 389 - }, - { - 'x': 1413568800000, - 'y': 312 - }, - { - 'x': 1413569400000, - 'y': 274 - }, - { - 'x': 1413570000000, - 'y': 285 - }, - { - 'x': 1413570600000, - 'y': 299 - }, - { - 'x': 1413571200000, - 'y': 207 - }, - { - 'x': 1413571800000, - 'y': 213 - }, - { - 'x': 1413572400000, - 'y': 119 - }, - { - 'x': 1413573600000, - 'y': 122 - }, - { - 'x': 1413574200000, - 'y': 169 - }, - { - 'x': 1413574800000, - 'y': 151 - }, - { - 'x': 1413575400000, - 'y': 152 - }, - { - 'x': 1413576000000, - 'y': 115 - }, - { - 'x': 1413576600000, - 'y': 117 - }, - { - 'x': 1413577200000, - 'y': 108 - }, - { - 'x': 1413577800000, - 'y': 100 - }, - { - 'x': 1413578400000, - 'y': 78 - }, - { - 'x': 1413579000000, - 'y': 88 - }, - { - 'x': 1413579600000, - 'y': 63 - }, - { - 'x': 1413580200000, - 'y': 58 - }, - { - 'x': 1413580800000, - 'y': 45 - }, - { - 'x': 1413581400000, - 'y': 57 - }, - { - 'x': 1413582000000, - 'y': 34 - }, - { - 'x': 1413582600000, - 'y': 41 - }, - { - 'x': 1413583200000, - 'y': 24 - }, - { - 'x': 1413583800000, - 'y': 27 - }, - { - 'x': 1413584400000, - 'y': 19 - }, - { - 'x': 1413585000000, - 'y': 24 - }, - { - 'x': 1413585600000, - 'y': 18 - }, - { - 'x': 1413586200000, - 'y': 17 - }, - { - 'x': 1413586800000, - 'y': 14 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1413543600000, - 'y': 90 - }, - { - 'x': 1413544200000, - 'y': 949 - }, - { - 'x': 1413544800000, - 'y': 1012 - }, - { - 'x': 1413545400000, - 'y': 1027 - }, - { - 'x': 1413546000000, - 'y': 1073 - }, - { - 'x': 1413546600000, - 'y': 992 - }, - { - 'x': 1413547200000, - 'y': 1005 - }, - { - 'x': 1413547800000, - 'y': 1014 - }, - { - 'x': 1413548400000, - 'y': 987 - }, - { - 'x': 1413549000000, - 'y': 982 - }, - { - 'x': 1413549600000, - 'y': 1086 - }, - { - 'x': 1413550200000, - 'y': 998 - }, - { - 'x': 1413550800000, - 'y': 935 - }, - { - 'x': 1413551400000, - 'y': 995 - }, - { - 'x': 1413552000000, - 'y': 926 - }, - { - 'x': 1413552600000, - 'y': 897 - }, - { - 'x': 1413553200000, - 'y': 873 - }, - { - 'x': 1413553800000, - 'y': 885 - }, - { - 'x': 1413554400000, - 'y': 859 - }, - { - 'x': 1413555000000, - 'y': 852 - }, - { - 'x': 1413555600000, - 'y': 779 - }, - { - 'x': 1413556200000, - 'y': 739 - }, - { - 'x': 1413556800000, - 'y': 783 - }, - { - 'x': 1413557400000, - 'y': 784 - }, - { - 'x': 1413558000000, - 'y': 687 - }, - { - 'x': 1413558600000, - 'y': 660 - }, - { - 'x': 1413559200000, - 'y': 672 - }, - { - 'x': 1413559800000, - 'y': 600 - }, - { - 'x': 1413560400000, - 'y': 659 - }, - { - 'x': 1413561000000, - 'y': 540 - }, - { - 'x': 1413561600000, - 'y': 539 - }, - { - 'x': 1413562200000, - 'y': 481 - }, - { - 'x': 1413562800000, - 'y': 498 - }, - { - 'x': 1413563400000, - 'y': 444 - }, - { - 'x': 1413564000000, - 'y': 452 - }, - { - 'x': 1413564600000, - 'y': 408 - }, - { - 'x': 1413565200000, - 'y': 358 - }, - { - 'x': 1413565800000, - 'y': 321 - }, - { - 'x': 1413566400000, - 'y': 305 - }, - { - 'x': 1413567000000, - 'y': 292 - }, - { - 'x': 1413567600000, - 'y': 289 - }, - { - 'x': 1413568200000, - 'y': 239 - }, - { - 'x': 1413568800000, - 'y': 256 - }, - { - 'x': 1413569400000, - 'y': 220 - }, - { - 'x': 1413570000000, - 'y': 205 - }, - { - 'x': 1413570600000, - 'y': 201 - }, - { - 'x': 1413571200000, - 'y': 183 - }, - { - 'x': 1413571800000, - 'y': 172 - }, - { - 'x': 1413572400000, - 'y': 73 - }, - { - 'x': 1413573600000, - 'y': 90 - }, - { - 'x': 1413574200000, - 'y': 130 - }, - { - 'x': 1413574800000, - 'y': 104 - }, - { - 'x': 1413575400000, - 'y': 108 - }, - { - 'x': 1413576000000, - 'y': 92 - }, - { - 'x': 1413576600000, - 'y': 79 - }, - { - 'x': 1413577200000, - 'y': 90 - }, - { - 'x': 1413577800000, - 'y': 72 - }, - { - 'x': 1413578400000, - 'y': 68 - }, - { - 'x': 1413579000000, - 'y': 52 - }, - { - 'x': 1413579600000, - 'y': 60 - }, - { - 'x': 1413580200000, - 'y': 51 - }, - { - 'x': 1413580800000, - 'y': 32 - }, - { - 'x': 1413581400000, - 'y': 37 - }, - { - 'x': 1413582000000, - 'y': 30 - }, - { - 'x': 1413582600000, - 'y': 29 - }, - { - 'x': 1413583200000, - 'y': 24 - }, - { - 'x': 1413583800000, - 'y': 16 - }, - { - 'x': 1413584400000, - 'y': 15 - }, - { - 'x': 1413585000000, - 'y': 15 - }, - { - 'x': 1413585600000, - 'y': 10 - }, - { - 'x': 1413586200000, - 'y': 9 - }, - { - 'x': 1413586800000, - 'y': 9 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1413543600000, - 'y': 44 - }, - { - 'x': 1413544200000, - 'y': 495 - }, - { - 'x': 1413544800000, - 'y': 489 - }, - { - 'x': 1413545400000, - 'y': 492 - }, - { - 'x': 1413546000000, - 'y': 556 - }, - { - 'x': 1413546600000, - 'y': 536 - }, - { - 'x': 1413547200000, - 'y': 511 - }, - { - 'x': 1413547800000, - 'y': 479 - }, - { - 'x': 1413548400000, - 'y': 544 - }, - { - 'x': 1413549000000, - 'y': 513 - }, - { - 'x': 1413549600000, - 'y': 501 - }, - { - 'x': 1413550200000, - 'y': 532 - }, - { - 'x': 1413550800000, - 'y': 440 - }, - { - 'x': 1413551400000, - 'y': 455 - }, - { - 'x': 1413552000000, - 'y': 455 - }, - { - 'x': 1413552600000, - 'y': 471 - }, - { - 'x': 1413553200000, - 'y': 428 - }, - { - 'x': 1413553800000, - 'y': 457 - }, - { - 'x': 1413554400000, - 'y': 450 - }, - { - 'x': 1413555000000, - 'y': 418 - }, - { - 'x': 1413555600000, - 'y': 398 - }, - { - 'x': 1413556200000, - 'y': 397 - }, - { - 'x': 1413556800000, - 'y': 359 - }, - { - 'x': 1413557400000, - 'y': 398 - }, - { - 'x': 1413558000000, - 'y': 339 - }, - { - 'x': 1413558600000, - 'y': 363 - }, - { - 'x': 1413559200000, - 'y': 297 - }, - { - 'x': 1413559800000, - 'y': 323 - }, - { - 'x': 1413560400000, - 'y': 302 - }, - { - 'x': 1413561000000, - 'y': 260 - }, - { - 'x': 1413561600000, - 'y': 276 - }, - { - 'x': 1413562200000, - 'y': 249 - }, - { - 'x': 1413562800000, - 'y': 248 - }, - { - 'x': 1413563400000, - 'y': 235 - }, - { - 'x': 1413564000000, - 'y': 234 - }, - { - 'x': 1413564600000, - 'y': 188 - }, - { - 'x': 1413565200000, - 'y': 192 - }, - { - 'x': 1413565800000, - 'y': 173 - }, - { - 'x': 1413566400000, - 'y': 160 - }, - { - 'x': 1413567000000, - 'y': 137 - }, - { - 'x': 1413567600000, - 'y': 158 - }, - { - 'x': 1413568200000, - 'y': 111 - }, - { - 'x': 1413568800000, - 'y': 145 - }, - { - 'x': 1413569400000, - 'y': 118 - }, - { - 'x': 1413570000000, - 'y': 104 - }, - { - 'x': 1413570600000, - 'y': 80 - }, - { - 'x': 1413571200000, - 'y': 79 - }, - { - 'x': 1413571800000, - 'y': 86 - }, - { - 'x': 1413572400000, - 'y': 47 - }, - { - 'x': 1413573600000, - 'y': 49 - }, - { - 'x': 1413574200000, - 'y': 68 - }, - { - 'x': 1413574800000, - 'y': 78 - }, - { - 'x': 1413575400000, - 'y': 77 - }, - { - 'x': 1413576000000, - 'y': 50 - }, - { - 'x': 1413576600000, - 'y': 51 - }, - { - 'x': 1413577200000, - 'y': 40 - }, - { - 'x': 1413577800000, - 'y': 42 - }, - { - 'x': 1413578400000, - 'y': 29 - }, - { - 'x': 1413579000000, - 'y': 24 - }, - { - 'x': 1413579600000, - 'y': 30 - }, - { - 'x': 1413580200000, - 'y': 18 - }, - { - 'x': 1413580800000, - 'y': 15 - }, - { - 'x': 1413581400000, - 'y': 19 - }, - { - 'x': 1413582000000, - 'y': 18 - }, - { - 'x': 1413582600000, - 'y': 13 - }, - { - 'x': 1413583200000, - 'y': 11 - }, - { - 'x': 1413583800000, - 'y': 11 - }, - { - 'x': 1413584400000, - 'y': 13 - }, - { - 'x': 1413585000000, - 'y': 9 - }, - { - 'x': 1413585600000, - 'y': 9 - }, - { - 'x': 1413586200000, - 'y': 9 - }, - { - 'x': 1413586800000, - 'y': 3 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1413543600000, - 'y': 35 - }, - { - 'x': 1413544200000, - 'y': 360 - }, - { - 'x': 1413544800000, - 'y': 343 - }, - { - 'x': 1413545400000, - 'y': 329 - }, - { - 'x': 1413546000000, - 'y': 345 - }, - { - 'x': 1413546600000, - 'y': 336 - }, - { - 'x': 1413547200000, - 'y': 330 - }, - { - 'x': 1413547800000, - 'y': 334 - }, - { - 'x': 1413548400000, - 'y': 326 - }, - { - 'x': 1413549000000, - 'y': 351 - }, - { - 'x': 1413549600000, - 'y': 334 - }, - { - 'x': 1413550200000, - 'y': 351 - }, - { - 'x': 1413550800000, - 'y': 337 - }, - { - 'x': 1413551400000, - 'y': 306 - }, - { - 'x': 1413552000000, - 'y': 346 - }, - { - 'x': 1413552600000, - 'y': 317 - }, - { - 'x': 1413553200000, - 'y': 298 - }, - { - 'x': 1413553800000, - 'y': 288 - }, - { - 'x': 1413554400000, - 'y': 283 - }, - { - 'x': 1413555000000, - 'y': 262 - }, - { - 'x': 1413555600000, - 'y': 245 - }, - { - 'x': 1413556200000, - 'y': 259 - }, - { - 'x': 1413556800000, - 'y': 267 - }, - { - 'x': 1413557400000, - 'y': 230 - }, - { - 'x': 1413558000000, - 'y': 218 - }, - { - 'x': 1413558600000, - 'y': 241 - }, - { - 'x': 1413559200000, - 'y': 213 - }, - { - 'x': 1413559800000, - 'y': 239 - }, - { - 'x': 1413560400000, - 'y': 208 - }, - { - 'x': 1413561000000, - 'y': 187 - }, - { - 'x': 1413561600000, - 'y': 166 - }, - { - 'x': 1413562200000, - 'y': 154 - }, - { - 'x': 1413562800000, - 'y': 184 - }, - { - 'x': 1413563400000, - 'y': 148 - }, - { - 'x': 1413564000000, - 'y': 153 - }, - { - 'x': 1413564600000, - 'y': 149 - }, - { - 'x': 1413565200000, - 'y': 102 - }, - { - 'x': 1413565800000, - 'y': 110 - }, - { - 'x': 1413566400000, - 'y': 121 - }, - { - 'x': 1413567000000, - 'y': 120 - }, - { - 'x': 1413567600000, - 'y': 86 - }, - { - 'x': 1413568200000, - 'y': 96 - }, - { - 'x': 1413568800000, - 'y': 71 - }, - { - 'x': 1413569400000, - 'y': 92 - }, - { - 'x': 1413570000000, - 'y': 65 - }, - { - 'x': 1413570600000, - 'y': 54 - }, - { - 'x': 1413571200000, - 'y': 68 - }, - { - 'x': 1413571800000, - 'y': 57 - }, - { - 'x': 1413572400000, - 'y': 33 - }, - { - 'x': 1413573600000, - 'y': 47 - }, - { - 'x': 1413574200000, - 'y': 42 - }, - { - 'x': 1413574800000, - 'y': 39 - }, - { - 'x': 1413575400000, - 'y': 25 - }, - { - 'x': 1413576000000, - 'y': 31 - }, - { - 'x': 1413576600000, - 'y': 37 - }, - { - 'x': 1413577200000, - 'y': 35 - }, - { - 'x': 1413577800000, - 'y': 19 - }, - { - 'x': 1413578400000, - 'y': 15 - }, - { - 'x': 1413579000000, - 'y': 21 - }, - { - 'x': 1413579600000, - 'y': 16 - }, - { - 'x': 1413580200000, - 'y': 18 - }, - { - 'x': 1413580800000, - 'y': 10 - }, - { - 'x': 1413581400000, - 'y': 13 - }, - { - 'x': 1413582000000, - 'y': 14 - }, - { - 'x': 1413582600000, - 'y': 11 - }, - { - 'x': 1413583200000, - 'y': 4 - }, - { - 'x': 1413583800000, - 'y': 6 - }, - { - 'x': 1413584400000, - 'y': 3 - }, - { - 'x': 1413585000000, - 'y': 6 - }, - { - 'x': 1413585600000, - 'y': 6 - }, - { - 'x': 1413586200000, - 'y': 2 - }, - { - 'x': 1413586800000, - 'y': 3 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1413543600000, - 'y': 21 - }, - { - 'x': 1413544200000, - 'y': 191 - }, - { - 'x': 1413544800000, - 'y': 176 - }, - { - 'x': 1413545400000, - 'y': 166 - }, - { - 'x': 1413546000000, - 'y': 183 - }, - { - 'x': 1413546600000, - 'y': 170 - }, - { - 'x': 1413547200000, - 'y': 153 - }, - { - 'x': 1413547800000, - 'y': 202 - }, - { - 'x': 1413548400000, - 'y': 175 - }, - { - 'x': 1413549000000, - 'y': 161 - }, - { - 'x': 1413549600000, - 'y': 174 - }, - { - 'x': 1413550200000, - 'y': 167 - }, - { - 'x': 1413550800000, - 'y': 171 - }, - { - 'x': 1413551400000, - 'y': 176 - }, - { - 'x': 1413552000000, - 'y': 139 - }, - { - 'x': 1413552600000, - 'y': 145 - }, - { - 'x': 1413553200000, - 'y': 157 - }, - { - 'x': 1413553800000, - 'y': 148 - }, - { - 'x': 1413554400000, - 'y': 149 - }, - { - 'x': 1413555000000, - 'y': 135 - }, - { - 'x': 1413555600000, - 'y': 118 - }, - { - 'x': 1413556200000, - 'y': 142 - }, - { - 'x': 1413556800000, - 'y': 141 - }, - { - 'x': 1413557400000, - 'y': 146 - }, - { - 'x': 1413558000000, - 'y': 114 - }, - { - 'x': 1413558600000, - 'y': 115 - }, - { - 'x': 1413559200000, - 'y': 136 - }, - { - 'x': 1413559800000, - 'y': 106 - }, - { - 'x': 1413560400000, - 'y': 92 - }, - { - 'x': 1413561000000, - 'y': 97 - }, - { - 'x': 1413561600000, - 'y': 90 - }, - { - 'x': 1413562200000, - 'y': 69 - }, - { - 'x': 1413562800000, - 'y': 66 - }, - { - 'x': 1413563400000, - 'y': 93 - }, - { - 'x': 1413564000000, - 'y': 75 - }, - { - 'x': 1413564600000, - 'y': 68 - }, - { - 'x': 1413565200000, - 'y': 55 - }, - { - 'x': 1413565800000, - 'y': 73 - }, - { - 'x': 1413566400000, - 'y': 57 - }, - { - 'x': 1413567000000, - 'y': 48 - }, - { - 'x': 1413567600000, - 'y': 41 - }, - { - 'x': 1413568200000, - 'y': 39 - }, - { - 'x': 1413568800000, - 'y': 32 - }, - { - 'x': 1413569400000, - 'y': 33 - }, - { - 'x': 1413570000000, - 'y': 39 - }, - { - 'x': 1413570600000, - 'y': 35 - }, - { - 'x': 1413571200000, - 'y': 25 - }, - { - 'x': 1413571800000, - 'y': 28 - }, - { - 'x': 1413572400000, - 'y': 8 - }, - { - 'x': 1413573600000, - 'y': 13 - }, - { - 'x': 1413574200000, - 'y': 23 - }, - { - 'x': 1413574800000, - 'y': 19 - }, - { - 'x': 1413575400000, - 'y': 16 - }, - { - 'x': 1413576000000, - 'y': 22 - }, - { - 'x': 1413576600000, - 'y': 13 - }, - { - 'x': 1413577200000, - 'y': 21 - }, - { - 'x': 1413577800000, - 'y': 11 - }, - { - 'x': 1413578400000, - 'y': 12 - }, - { - 'x': 1413579000000, - 'y': 10 - }, - { - 'x': 1413579600000, - 'y': 7 - }, - { - 'x': 1413580200000, - 'y': 4 - }, - { - 'x': 1413580800000, - 'y': 5 - }, - { - 'x': 1413581400000, - 'y': 7 - }, - { - 'x': 1413582000000, - 'y': 9 - }, - { - 'x': 1413582600000, - 'y': 2 - }, - { - 'x': 1413583200000, - 'y': 2 - }, - { - 'x': 1413583800000, - 'y': 4 - }, - { - 'x': 1413584400000, - 'y': 6 - }, - { - 'x': 1413585600000, - 'y': 2 - }, - { - 'x': 1413586200000, - 'y': 4 - }, - { - 'x': 1413586800000, - 'y': 4 - } - ] - } - ], - 'hits': 108970, - 'xAxisOrderedValues': [ - 1413543600000, - 1413544200000, - 1413544800000, - 1413545400000, - 1413546000000, - 1413546600000, - 1413547200000, - 1413547800000, - 1413548400000, - 1413549000000, - 1413549600000, - 1413550200000, - 1413550800000, - 1413551400000, - 1413552000000, - 1413552600000, - 1413553200000, - 1413553800000, - 1413554400000, - 1413555000000, - 1413555600000, - 1413556200000, - 1413556800000, - 1413557400000, - 1413558000000, - 1413558600000, - 1413559200000, - 1413559800000, - 1413560400000, - 1413561000000, - 1413561600000, - 1413562200000, - 1413562800000, - 1413563400000, - 1413564000000, - 1413564600000, - 1413565200000, - 1413565800000, - 1413566400000, - 1413567000000, - 1413567600000, - 1413568200000, - 1413568800000, - 1413569400000, - 1413570000000, - 1413570600000, - 1413571200000, - 1413571800000, - 1413572400000, - 1413573600000, - 1413574200000, - 1413574800000, - 1413575400000, - 1413576000000, - 1413576600000, - 1413577200000, - 1413577800000, - 1413578400000, - 1413579000000, - 1413579600000, - 1413580200000, - 1413580800000, - 1413581400000, - 1413582000000, - 1413582600000, - 1413583200000, - 1413583800000, - 1413584400000, - 1413585000000, - 1413585600000, - 1413586200000, - 1413586800000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js deleted file mode 100644 index 041fad2ed15b9..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js +++ /dev/null @@ -1,112 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 10379 - }, - { - 'x': 'png', - 'y': 6395 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 9253 - }, - { - 'x': 'png', - 'y': 5571 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 7740 - }, - { - 'x': 'png', - 'y': 4697 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171443 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js deleted file mode 100644 index cc4f598c7b1b7..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '200: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 25260 - }, - { - 'x': 'png', - 'y': 15311 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '404: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 1352 - }, - { - 'x': 'png', - 'y': 826 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 761 - }, - { - 'x': 'png', - 'y': 527 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171443 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js deleted file mode 100644 index 2a2d14d59b67e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js +++ /dev/null @@ -1,42 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 27374 - }, - { - 'x': 'html', - 'y': 0 - }, - { - 'x': 'png', - 'y': 16663 - } - ] - } - ], - 'hits': 171454, - 'xAxisOrderedValues': ['css', 'html', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js deleted file mode 100644 index d283d79315177..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js +++ /dev/null @@ -1,3745 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'title': 'Top 2 geo.dest: CN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 42, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 42, - 'value': 42, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 21, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 21, - 'value': 21, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 19, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 19, - 'value': 19, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - -45 - ], - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -135, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 42 - } - } - }, - { - 'label': 'Top 2 geo.dest: IN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 32, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 32, - 'value': 32, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 28, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 28, - 'value': 28, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 27, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 27, - 'value': 27, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 24, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 24, - 'value': 24, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 17, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 17, - 'value': 17, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 16, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 16, - 'value': 16, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 14, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 14, - 'value': 14, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 13, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 13, - 'value': 13, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -90 - ], - [ - 180, - -90 - ], - [ - 180, - -45 - ], - [ - 135, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 32 - } - } - } - ] -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js deleted file mode 100644 index 4e65502f8d278..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js +++ /dev/null @@ -1,1847 +0,0 @@ -import _ from 'lodash'; - -export default { - 'valueFormatter': _.identity, - 'geohashGridAgg': { 'vis': { 'params': {} } }, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 608, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 608, - 'value': 608, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 522, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 522, - 'value': 522, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 90 - ], - [ - 0, - 135 - ], - [ - 45, - 135 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 517, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 517, - 'value': 517, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 446, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 446, - 'value': 446, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 426, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 426, - 'value': 426, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 413, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 413, - 'value': 413, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - 45, - 90 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 362, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 362, - 'value': 362, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 352, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 352, - 'value': 352, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -135 - ], - [ - 0, - -90 - ], - [ - 45, - -90 - ], - [ - 45, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 216, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 216, - 'value': 216, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 183, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 183, - 'value': 183, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 90, - 90 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 158, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 158, - 'value': 158, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 135 - ], - [ - -45, - 180 - ], - [ - 0, - 180 - ], - [ - 0, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 139, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 139, - 'value': 139, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 90 - ], - [ - 45, - 135 - ], - [ - 90, - 135 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 110, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 110, - 'value': 110, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -135 - ], - [ - 45, - -90 - ], - [ - 90, - -90 - ], - [ - 90, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 101, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 101, - 'value': 101, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 90 - ], - [ - -45, - 135 - ], - [ - 0, - 135 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 101, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 101, - 'value': 101, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 92, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 92, - 'value': 92, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -90 - ], - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 75, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 75, - 'value': 75, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -180 - ], - [ - 45, - -135 - ], - [ - 90, - -135 - ], - [ - 90, - -180 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 64, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 64, - 'value': 64, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 36, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 36, - 'value': 36, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 135 - ], - [ - 45, - 180 - ], - [ - 90, - 180 - ], - [ - 90, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 34, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 34, - 'value': 34, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 135 - ], - [ - 0, - 180 - ], - [ - 45, - 180 - ], - [ - 45, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 16, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 16, - 'value': 16, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - 0, - 90 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 135 - ], - [ - -90, - 180 - ], - [ - -45, - 180 - ], - [ - -45, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': '2', - 'center': [ - -157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '2', - 'value': '2', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -180 - ], - [ - -45, - -135 - ], - [ - 0, - -135 - ], - [ - 0, - -180 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'h', - 'center': [ - 22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'h', - 'value': 'h', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'n', - 'center': [ - 112.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'n', - 'value': 'n', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 90 - ], - [ - -90, - 135 - ], - [ - -45, - 135 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'j', - 'center': [ - 67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'j', - 'value': 'j', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -45, - 90 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -135 - ], - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '1', - 'center': [ - -112.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '1', - 'value': '1', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -135 - ], - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -135 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 608, - 'zoom': 2, - 'center': [5, 15] - } - }, -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js deleted file mode 100644 index 64deea0e391a6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js +++ /dev/null @@ -1,3667 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'title': 'Top 2 geo.dest: CN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 39, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 39, - 'value': 39, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 20, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 20, - 'value': 20, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - -45 - ], - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -135, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 39 - } - } - }, - { - 'label': 'Top 2 geo.dest: IN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 29, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 29, - 'value': 29, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 28, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 28, - 'value': 28, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 24, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 24, - 'value': 24, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 20, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 20, - 'value': 20, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 14, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 14, - 'value': 14, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -90 - ], - [ - 180, - -90 - ], - [ - 180, - -45 - ], - [ - 135, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 31 - } - } - } - ], - 'hits': 1639 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js deleted file mode 100644 index 96d2cfd174579..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js +++ /dev/null @@ -1,368 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': '404: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 1, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 0, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 0, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 0, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 1, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 0, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 0, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '200: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 2, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 3, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 3, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 1, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 1, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 4, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 1, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 1, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 2, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 3, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 4, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 5, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 2, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 2, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 0, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 0, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 0, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 1, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 0, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 0, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 2147483600, - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 8589934500, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - ], - 'hits': 40 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js deleted file mode 100644 index 27050030ebdfd..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js +++ /dev/null @@ -1,212 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '404: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 1 - }, - { - 'x': 10737418200, - 'y': 1 - }, - { - 'x': 15032385500, - 'y': 2 - }, - { - 'x': 19327352800, - 'y': 1 - }, - { - 'x': 32212254700, - 'y': 1 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '200: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 3221225400, - 'y': 4 - }, - { - 'x': 4294967200, - 'y': 3 - }, - { - 'x': 5368709100, - 'y': 3 - }, - { - 'x': 6442450900, - 'y': 2 - }, - { - 'x': 7516192700, - 'y': 2 - }, - { - 'x': 8589934500, - 'y': 2 - }, - { - 'x': 9663676400, - 'y': 3 - }, - { - 'x': 11811160000, - 'y': 3 - }, - { - 'x': 12884901800, - 'y': 2 - }, - { - 'x': 13958643700, - 'y': 1 - }, - { - 'x': 15032385500, - 'y': 2 - }, - { - 'x': 16106127300, - 'y': 3 - }, - { - 'x': 17179869100, - 'y': 1 - }, - { - 'x': 18253611000, - 'y': 4 - }, - { - 'x': 19327352800, - 'y': 1 - }, - { - 'x': 20401094600, - 'y': 1 - }, - { - 'x': 21474836400, - 'y': 4 - }, - { - 'x': 32212254700, - 'y': 3 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 10737418200, - 'y': 1 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 2147483600, - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 8589934500, - 9663676400, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 17179869100, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - 32212254700, - ], - 'hits': 51 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js deleted file mode 100644 index 5c7554db2060d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js +++ /dev/null @@ -1,124 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 3221225400, - 'y': 5 - }, - { - 'x': 4294967200, - 'y': 2 - }, - { - 'x': 5368709100, - 'y': 5 - }, - { - 'x': 6442450900, - 'y': 4 - }, - { - 'x': 7516192700, - 'y': 1 - }, - { - 'x': 9663676400, - 'y': 9 - }, - { - 'x': 10737418200, - 'y': 5 - }, - { - 'x': 11811160000, - 'y': 5 - }, - { - 'x': 12884901800, - 'y': 2 - }, - { - 'x': 13958643700, - 'y': 3 - }, - { - 'x': 15032385500, - 'y': 3 - }, - { - 'x': 16106127300, - 'y': 3 - }, - { - 'x': 17179869100, - 'y': 1 - }, - { - 'x': 18253611000, - 'y': 6 - }, - { - 'x': 19327352800, - 'y': 3 - }, - { - 'x': 20401094600, - 'y': 3 - }, - { - 'x': 21474836400, - 'y': 7 - }, - { - 'x': 32212254700, - 'y': 4 - } - ] - } - ], - 'hits': 71, - 'xAxisOrderedValues': [ - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 9663676400, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 17179869100, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - 32212254700, - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js deleted file mode 100644 index c47155840cec5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js +++ /dev/null @@ -1,309 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'slices': { - 'children': [ - { - 'name': 0, - 'size': 378611, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 1000, - 'size': 205997, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 2000, - 'size': 397189, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 3000, - 'size': 397195, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 4000, - 'size': 398429, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 5000, - 'size': 397843, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 6000, - 'size': 398140, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 7000, - 'size': 398076, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 8000, - 'size': 396746, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 9000, - 'size': 397418, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 10000, - 'size': 20222, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 11000, - 'size': 20173, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 12000, - 'size': 20026, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 13000, - 'size': 19986, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 14000, - 'size': 20091, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 15000, - 'size': 20052, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 16000, - 'size': 20349, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 17000, - 'size': 20290, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 18000, - 'size': 20399, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 19000, - 'size': 20133, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 20000, - 'size': 9, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - } - ] - }, - 'names': [ - 0, - 1000, - 2000, - 3000, - 4000, - 5000, - 6000, - 7000, - 8000, - 9000, - 10000, - 11000, - 12000, - 13000, - 14000, - 15000, - 16000, - 17000, - 18000, - 19000, - 20000 - ], - 'hits': 3967374, - 'tooltipFormatter': function (event) { - return event.point; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js deleted file mode 100644 index b05e258133963..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': '', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '_all', - 'y': 274 - } - ] - } - ], - 'hits': 274, - 'xAxisOrderedValues': ['_all'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js deleted file mode 100644 index d6f3fc4361f32..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js +++ /dev/null @@ -1,62 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'apache: _type', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 13309 - }, - { - 'x': '1000.0-2000.0', - 'y': 7196 - } - ] - } - ] - }, - { - 'label': 'nginx: _type', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 3278 - }, - { - 'x': '1000.0-2000.0', - 'y': 1804 - } - ] - } - ] - } - ], - 'hits': 171499, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js deleted file mode 100644 index b420565b1c96b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js +++ /dev/null @@ -1,88 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 6422, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 3446, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 5430, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 3010, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 4735, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 2542, - 'y0': 0 - } - ] - } - ] - } - ], - 'hits': 171501, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js deleted file mode 100644 index 2ac35efadc8f2..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js +++ /dev/null @@ -1,38 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 16576 - }, - { - 'x': '1000.0-2000.0', - 'y': 9005 - } - ] - } - ], - 'hits': 171500, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js deleted file mode 100644 index 5b1e29ac0d54a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js +++ /dev/null @@ -1,242 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'http: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128237 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10258 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'info: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 108148 - }, - { - 'x': 'info', - 'y': 96242 - }, - { - 'x': 'security', - 'y': 25889 - }, - { - 'x': 'error', - 'y': 7673 - }, - { - 'x': 'warning', - 'y': 12842 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.slate.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 98056 - }, - { - 'x': 'info', - 'y': 87344 - }, - { - 'x': 'security', - 'y': 23577 - }, - { - 'x': 'error', - 'y': 7004 - }, - { - 'x': 'warning', - 'y': 11759 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'twitter.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 74154 - }, - { - 'x': 'info', - 'y': 65963 - }, - { - 'x': 'security', - 'y': 17832 - }, - { - 'x': 'error', - 'y': 5258 - }, - { - 'x': 'warning', - 'y': 8906 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.www.slate.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 62591 - }, - { - 'x': 'info', - 'y': 55822 - }, - { - 'x': 'security', - 'y': 15100 - }, - { - 'x': 'error', - 'y': 4564 - }, - { - 'x': 'warning', - 'y': 7498 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171446 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js deleted file mode 100644 index 147eb691eb67b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js +++ /dev/null @@ -1,242 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': 'h3: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'h5: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'http: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'success: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 120689 - }, - { - 'x': 'info', - 'y': 107621 - }, - { - 'x': 'security', - 'y': 28916 - }, - { - 'x': 'error', - 'y': 8590 - }, - { - 'x': 'warning', - 'y': 14548 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.slate.com: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 62292 - }, - { - 'x': 'info', - 'y': 55646 - }, - { - 'x': 'security', - 'y': 14823 - }, - { - 'x': 'error', - 'y': 4441 - }, - { - 'x': 'warning', - 'y': 7539 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171445 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js deleted file mode 100644 index 3691d854c6c2a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js +++ /dev/null @@ -1,49 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 143995 - }, - { - 'x': 'info', - 'y': 128233 - }, - { - 'x': 'security', - 'y': 34515 - }, - { - 'x': 'error', - 'y': 10256 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'hits': 171439, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js deleted file mode 100644 index 228a22ed534d5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js +++ /dev/null @@ -1,1635 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1416850340336, - 'max': 1416852140336 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisOrderedValues': [ - 1416850320000, - 1416850350000, - 1416850380000, - 1416850410000, - 1416850440000, - 1416850470000, - 1416850500000, - 1416850530000, - 1416850560000, - 1416850590000, - 1416850620000, - 1416850650000, - 1416850680000, - 1416850710000, - 1416850740000, - 1416850770000, - 1416850800000, - 1416850830000, - 1416850860000, - 1416850890000, - 1416850920000, - 1416850950000, - 1416850980000, - 1416851010000, - 1416851040000, - 1416851070000, - 1416851100000, - 1416851130000, - 1416851160000, - 1416851190000, - 1416851220000, - 1416851250000, - 1416851280000, - 1416851310000, - 1416851340000, - 1416851370000, - 1416851400000, - 1416851430000, - 1416851460000, - 1416851490000, - 1416851520000, - 1416851550000, - 1416851580000, - 1416851610000, - 1416851640000, - 1416851670000, - 1416851700000, - 1416851730000, - 1416851760000, - 1416851790000, - 1416851820000, - 1416851850000, - 1416851880000, - 1416851910000, - 1416851940000, - 1416851970000, - 1416852000000, - 1416852030000, - 1416852060000, - 1416852090000, - 1416852120000, - ], - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1416850320000, - 'y': 110, - 'y0': 0 - }, - { - 'x': 1416850350000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850380000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416850410000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416850440000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416850470000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850500000, - 'y': 16, - 'y0': 0 - }, - { - 'x': 1416850530000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416850560000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850590000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850620000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850650000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850680000, - 'y': 31, - 'y0': 0 - }, - { - 'x': 1416850710000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850740000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850770000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850800000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416850830000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416850860000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850890000, - 'y': 19, - 'y0': 0 - }, - { - 'x': 1416850920000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416850950000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850980000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851010000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416851040000, - 'y': 22, - 'y0': 0 - }, - { - 'x': 1416851070000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851100000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416851130000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851160000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851190000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851220000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851250000, - 'y': 36, - 'y0': 0 - }, - { - 'x': 1416851280000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851310000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851340000, - 'y': 22, - 'y0': 0 - }, - { - 'x': 1416851370000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851400000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416851430000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851460000, - 'y': 39, - 'y0': 0 - }, - { - 'x': 1416851490000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851520000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851550000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851580000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851610000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851640000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851670000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851700000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851730000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416851760000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416851790000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416851820000, - 'y': 26, - 'y0': 0 - }, - { - 'x': 1416851850000, - 'y': 26, - 'y0': 0 - }, - { - 'x': 1416851880000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416851910000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416851940000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851970000, - 'y': 46, - 'y0': 0 - }, - { - 'x': 1416852000000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416852030000, - 'y': 20, - 'y0': 0 - }, - { - 'x': 1416852060000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416852090000, - 'y': 15, - 'y0': 0 - }, - { - 'x': 1416852120000, - 'y': 18, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1416850320000, - 'y': 3, - 'y0': 11 - }, - { - 'x': 1416850350000, - 'y': 13, - 'y0': 24 - }, - { - 'x': 1416850380000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416850410000, - 'y': 12, - 'y0': 21 - }, - { - 'x': 1416850440000, - 'y': 9, - 'y0': 32 - }, - { - 'x': 1416850470000, - 'y': 12, - 'y0': 24 - }, - { - 'x': 1416850500000, - 'y': 6, - 'y0': 16 - }, - { - 'x': 1416850530000, - 'y': 6, - 'y0': 27 - }, - { - 'x': 1416850560000, - 'y': 11, - 'y0': 24 - }, - { - 'x': 1416850590000, - 'y': 11, - 'y0': 38 - }, - { - 'x': 1416850620000, - 'y': 6, - 'y0': 33 - }, - { - 'x': 1416850650000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416850680000, - 'y': 6, - 'y0': 31 - }, - { - 'x': 1416850710000, - 'y': 4, - 'y0': 24 - }, - { - 'x': 1416850740000, - 'y': 9, - 'y0': 24 - }, - { - 'x': 1416850770000, - 'y': 3, - 'y0': 38 - }, - { - 'x': 1416850800000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416850830000, - 'y': 6, - 'y0': 30 - }, - { - 'x': 1416850860000, - 'y': 9, - 'y0': 38 - }, - { - 'x': 1416850890000, - 'y': 5, - 'y0': 19 - }, - { - 'x': 1416850920000, - 'y': 8, - 'y0': 23 - }, - { - 'x': 1416850950000, - 'y': 9, - 'y0': 33 - }, - { - 'x': 1416850980000, - 'y': 5, - 'y0': 28 - }, - { - 'x': 1416851010000, - 'y': 6, - 'y0': 24 - }, - { - 'x': 1416851040000, - 'y': 9, - 'y0': 22 - }, - { - 'x': 1416851070000, - 'y': 9, - 'y0': 28 - }, - { - 'x': 1416851100000, - 'y': 11, - 'y0': 27 - }, - { - 'x': 1416851130000, - 'y': 5, - 'y0': 32 - }, - { - 'x': 1416851160000, - 'y': 8, - 'y0': 32 - }, - { - 'x': 1416851190000, - 'y': 6, - 'y0': 30 - }, - { - 'x': 1416851220000, - 'y': 10, - 'y0': 32 - }, - { - 'x': 1416851250000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416851280000, - 'y': 6, - 'y0': 32 - }, - { - 'x': 1416851310000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851340000, - 'y': 8, - 'y0': 22 - }, - { - 'x': 1416851370000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851400000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416851430000, - 'y': 10, - 'y0': 28 - }, - { - 'x': 1416851460000, - 'y': 5, - 'y0': 39 - }, - { - 'x': 1416851490000, - 'y': 7, - 'y0': 28 - }, - { - 'x': 1416851520000, - 'y': 6, - 'y0': 28 - }, - { - 'x': 1416851550000, - 'y': 4, - 'y0': 28 - }, - { - 'x': 1416851580000, - 'y': 9, - 'y0': 30 - }, - { - 'x': 1416851610000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851640000, - 'y': 9, - 'y0': 30 - }, - { - 'x': 1416851670000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416851700000, - 'y': 11, - 'y0': 23 - }, - { - 'x': 1416851730000, - 'y': 4, - 'y0': 27 - }, - { - 'x': 1416851760000, - 'y': 8, - 'y0': 21 - }, - { - 'x': 1416851790000, - 'y': 5, - 'y0': 24 - }, - { - 'x': 1416851820000, - 'y': 7, - 'y0': 26 - }, - { - 'x': 1416851850000, - 'y': 7, - 'y0': 26 - }, - { - 'x': 1416851880000, - 'y': 4, - 'y0': 21 - }, - { - 'x': 1416851910000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416851940000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416851970000, - 'y': 6, - 'y0': 46 - }, - { - 'x': 1416852000000, - 'y': 3, - 'y0': 27 - }, - { - 'x': 1416852030000, - 'y': 6, - 'y0': 20 - }, - { - 'x': 1416852060000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416852090000, - 'y': 5, - 'y0': 15 - }, - { - 'x': 1416852120000, - 'y': 1, - 'y0': 18 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1416850320000, - 'y': 1, - 'y0': 14 - }, - { - 'x': 1416850350000, - 'y': 2, - 'y0': 37 - }, - { - 'x': 1416850380000, - 'y': 4, - 'y0': 39 - }, - { - 'x': 1416850410000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416850440000, - 'y': 3, - 'y0': 41 - }, - { - 'x': 1416850470000, - 'y': 1, - 'y0': 36 - }, - { - 'x': 1416850500000, - 'y': 1, - 'y0': 22 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416850560000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416850590000, - 'y': 5, - 'y0': 49 - }, - { - 'x': 1416850620000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416850650000, - 'y': 1, - 'y0': 41 - }, - { - 'x': 1416850680000, - 'y': 4, - 'y0': 37 - }, - { - 'x': 1416850710000, - 'y': 1, - 'y0': 28 - }, - { - 'x': 1416850740000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850800000, - 'y': 2, - 'y0': 39 - }, - { - 'x': 1416850830000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416850860000, - 'y': 3, - 'y0': 47 - }, - { - 'x': 1416850890000, - 'y': 1, - 'y0': 24 - }, - { - 'x': 1416850920000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416850950000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416850980000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851010000, - 'y': 5, - 'y0': 30 - }, - { - 'x': 1416851040000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416851070000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851100000, - 'y': 5, - 'y0': 38 - }, - { - 'x': 1416851130000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851160000, - 'y': 4, - 'y0': 40 - }, - { - 'x': 1416851190000, - 'y': 9, - 'y0': 36 - }, - { - 'x': 1416851220000, - 'y': 7, - 'y0': 42 - }, - { - 'x': 1416851250000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851280000, - 'y': 1, - 'y0': 38 - }, - { - 'x': 1416851310000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416851340000, - 'y': 5, - 'y0': 30 - }, - { - 'x': 1416851370000, - 'y': 3, - 'y0': 32 - }, - { - 'x': 1416851400000, - 'y': 5, - 'y0': 41 - }, - { - 'x': 1416851430000, - 'y': 4, - 'y0': 38 - }, - { - 'x': 1416851460000, - 'y': 5, - 'y0': 44 - }, - { - 'x': 1416851490000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416851520000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416851550000, - 'y': 4, - 'y0': 32 - }, - { - 'x': 1416851580000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851610000, - 'y': 4, - 'y0': 32 - }, - { - 'x': 1416851640000, - 'y': 0, - 'y0': 39 - }, - { - 'x': 1416851670000, - 'y': 2, - 'y0': 29 - }, - { - 'x': 1416851700000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416851730000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416851760000, - 'y': 0, - 'y0': 29 - }, - { - 'x': 1416851790000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851820000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851850000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851880000, - 'y': 0, - 'y0': 25 - }, - { - 'x': 1416851910000, - 'y': 0, - 'y0': 41 - }, - { - 'x': 1416851940000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851970000, - 'y': 3, - 'y0': 52 - }, - { - 'x': 1416852000000, - 'y': 1, - 'y0': 30 - }, - { - 'x': 1416852030000, - 'y': 5, - 'y0': 26 - }, - { - 'x': 1416852060000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416852090000, - 'y': 1, - 'y0': 20 - }, - { - 'x': 1416852120000, - 'y': 2, - 'y0': 19 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1416850320000, - 'y': 1, - 'y0': 15 - }, - { - 'x': 1416850350000, - 'y': 6, - 'y0': 39 - }, - { - 'x': 1416850380000, - 'y': 6, - 'y0': 43 - }, - { - 'x': 1416850410000, - 'y': 5, - 'y0': 35 - }, - { - 'x': 1416850440000, - 'y': 3, - 'y0': 44 - }, - { - 'x': 1416850470000, - 'y': 5, - 'y0': 37 - }, - { - 'x': 1416850500000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416850560000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416850590000, - 'y': 2, - 'y0': 54 - }, - { - 'x': 1416850620000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416850650000, - 'y': 1, - 'y0': 42 - }, - { - 'x': 1416850680000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850710000, - 'y': 5, - 'y0': 29 - }, - { - 'x': 1416850740000, - 'y': 7, - 'y0': 36 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850800000, - 'y': 3, - 'y0': 41 - }, - { - 'x': 1416850830000, - 'y': 6, - 'y0': 41 - }, - { - 'x': 1416850860000, - 'y': 2, - 'y0': 50 - }, - { - 'x': 1416850890000, - 'y': 4, - 'y0': 25 - }, - { - 'x': 1416850920000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416850950000, - 'y': 3, - 'y0': 46 - }, - { - 'x': 1416850980000, - 'y': 8, - 'y0': 36 - }, - { - 'x': 1416851010000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851040000, - 'y': 4, - 'y0': 33 - }, - { - 'x': 1416851070000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416851100000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416851130000, - 'y': 4, - 'y0': 40 - }, - { - 'x': 1416851160000, - 'y': 3, - 'y0': 44 - }, - { - 'x': 1416851190000, - 'y': 4, - 'y0': 45 - }, - { - 'x': 1416851220000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416851250000, - 'y': 4, - 'y0': 43 - }, - { - 'x': 1416851280000, - 'y': 8, - 'y0': 39 - }, - { - 'x': 1416851310000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851340000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851370000, - 'y': 7, - 'y0': 35 - }, - { - 'x': 1416851400000, - 'y': 2, - 'y0': 46 - }, - { - 'x': 1416851430000, - 'y': 3, - 'y0': 42 - }, - { - 'x': 1416851460000, - 'y': 3, - 'y0': 49 - }, - { - 'x': 1416851490000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851520000, - 'y': 4, - 'y0': 36 - }, - { - 'x': 1416851550000, - 'y': 3, - 'y0': 36 - }, - { - 'x': 1416851580000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416851610000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416851640000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851670000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416851700000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416851730000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416851760000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851790000, - 'y': 5, - 'y0': 33 - }, - { - 'x': 1416851820000, - 'y': 1, - 'y0': 36 - }, - { - 'x': 1416851850000, - 'y': 3, - 'y0': 36 - }, - { - 'x': 1416851880000, - 'y': 6, - 'y0': 25 - }, - { - 'x': 1416851910000, - 'y': 4, - 'y0': 41 - }, - { - 'x': 1416851940000, - 'y': 7, - 'y0': 32 - }, - { - 'x': 1416851970000, - 'y': 5, - 'y0': 55 - }, - { - 'x': 1416852000000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416852030000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416852060000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416852090000, - 'y': 6, - 'y0': 21 - }, - { - 'x': 1416852120000, - 'y': 2, - 'y0': 21 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1416850320000, - 'y': 0, - 'y0': 16 - }, - { - 'x': 1416850350000, - 'y': 1, - 'y0': 45 - }, - { - 'x': 1416850380000, - 'y': 0, - 'y0': 49 - }, - { - 'x': 1416850410000, - 'y': 2, - 'y0': 40 - }, - { - 'x': 1416850440000, - 'y': 0, - 'y0': 47 - }, - { - 'x': 1416850470000, - 'y': 0, - 'y0': 42 - }, - { - 'x': 1416850500000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 35 - }, - { - 'x': 1416850560000, - 'y': 3, - 'y0': 40 - }, - { - 'x': 1416850590000, - 'y': 2, - 'y0': 56 - }, - { - 'x': 1416850620000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850650000, - 'y': 5, - 'y0': 43 - }, - { - 'x': 1416850680000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850710000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416850740000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416850800000, - 'y': 1, - 'y0': 44 - }, - { - 'x': 1416850830000, - 'y': 1, - 'y0': 47 - }, - { - 'x': 1416850860000, - 'y': 1, - 'y0': 52 - }, - { - 'x': 1416850890000, - 'y': 1, - 'y0': 29 - }, - { - 'x': 1416850920000, - 'y': 2, - 'y0': 36 - }, - { - 'x': 1416850950000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416850980000, - 'y': 0, - 'y0': 44 - }, - { - 'x': 1416851010000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851040000, - 'y': 2, - 'y0': 37 - }, - { - 'x': 1416851070000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851100000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416851130000, - 'y': 0, - 'y0': 44 - }, - { - 'x': 1416851160000, - 'y': 1, - 'y0': 47 - }, - { - 'x': 1416851190000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416851220000, - 'y': 4, - 'y0': 51 - }, - { - 'x': 1416851250000, - 'y': 0, - 'y0': 47 - }, - { - 'x': 1416851280000, - 'y': 3, - 'y0': 47 - }, - { - 'x': 1416851310000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851340000, - 'y': 2, - 'y0': 39 - }, - { - 'x': 1416851370000, - 'y': 2, - 'y0': 42 - }, - { - 'x': 1416851400000, - 'y': 3, - 'y0': 48 - }, - { - 'x': 1416851430000, - 'y': 1, - 'y0': 45 - }, - { - 'x': 1416851460000, - 'y': 0, - 'y0': 52 - }, - { - 'x': 1416851490000, - 'y': 2, - 'y0': 40 - }, - { - 'x': 1416851520000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416851550000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851580000, - 'y': 1, - 'y0': 46 - }, - { - 'x': 1416851610000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851640000, - 'y': 1, - 'y0': 42 - }, - { - 'x': 1416851670000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416851700000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851730000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416851760000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416851790000, - 'y': 1, - 'y0': 38 - }, - { - 'x': 1416851820000, - 'y': 1, - 'y0': 37 - }, - { - 'x': 1416851850000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416851880000, - 'y': 1, - 'y0': 31 - }, - { - 'x': 1416851910000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416851940000, - 'y': 0, - 'y0': 39 - }, - { - 'x': 1416851970000, - 'y': 0, - 'y0': 60 - }, - { - 'x': 1416852000000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416852030000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416852060000, - 'y': 1, - 'y0': 46 - }, - { - 'x': 1416852090000, - 'y': 1, - 'y0': 27 - }, - { - 'x': 1416852120000, - 'y': 0, - 'y0': 23 - } - ] - } - ], - 'hits': 2595, - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js deleted file mode 100644 index 4683640725f2a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js +++ /dev/null @@ -1,146 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'logstash: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110710 - }, - { - 'x': 'css', - 'y': 27376 - }, - { - 'x': 'png', - 'y': 16664 - }, - { - 'x': 'gif', - 'y': 11264 - }, - { - 'x': 'php', - 'y': 5448 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '2014.11.12: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110643 - }, - { - 'x': 'css', - 'y': 27350 - }, - { - 'x': 'png', - 'y': 16648 - }, - { - 'x': 'gif', - 'y': 11257 - }, - { - 'x': 'php', - 'y': 5440 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '2014.11.11: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 67 - }, - { - 'x': 'css', - 'y': 26 - }, - { - 'x': 'png', - 'y': 16 - }, - { - 'x': 'gif', - 'y': 7 - }, - { - 'x': 'php', - 'y': 8 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'hits': 171462 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js deleted file mode 100644 index 2b4ee83eca44c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js +++ /dev/null @@ -1,100 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '0.0-1000.0: bytes', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 3378 - }, - { - 'x': 'css', - 'y': 762 - }, - { - 'x': 'png', - 'y': 527 - }, - { - 'x': 'gif', - 'y': 11258 - }, - { - 'x': 'php', - 'y': 653 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '1000.0-2000.0: bytes', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 6422 - }, - { - 'x': 'css', - 'y': 1591 - }, - { - 'x': 'png', - 'y': 430 - }, - { - 'x': 'gif', - 'y': 8 - }, - { - 'x': 'php', - 'y': 561 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'hits': 171458 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js deleted file mode 100644 index f717012d430cf..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js +++ /dev/null @@ -1,50 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'Top 5 extension', - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110710 - }, - { - 'x': 'css', - 'y': 27389 - }, - { - 'x': 'png', - 'y': 16661 - }, - { - 'x': 'gif', - 'y': 11269 - }, - { - 'x': 'php', - 'y': 5447 - } - ] - } - ], - 'hits': 171476, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js deleted file mode 100644 index 2b86663ab4673..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js +++ /dev/null @@ -1,72 +0,0 @@ -import _ from 'lodash'; - -export default { - 'xAxisOrderedValues': ['_all'], - 'yAxisLabel': 'Count', - 'zAxisLabel': 'machine.os.raw: Descending', - 'yScale': null, - 'series': [{ - 'label': 'ios', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 2820, - 'series': 'ios' - }] - }, { - 'label': 'win 7', - 'aggId': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 2319, - 'series': 'win 7' - }] - }, { - 'label': 'win 8', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 1835, - 'series': 'win 8' - }] - }, { - 'label': 'windows xp service pack 2 version 20123452', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 734, - 'series': 'win xp' - }] - }, { - 'label': 'osx', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 1352, - 'series': 'osx' - }] - }], - 'hits': 14005, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'yAxisFormatter': function (val) { - return val; - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js deleted file mode 100644 index 8e25015c10186..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import $ from 'jquery'; - -// Data -import series from '../fixtures/mock_data/date_histogram/_series'; -import columns from '../fixtures/mock_data/date_histogram/_columns'; -import rows from '../fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; - -import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; - -const dateHistogramArray = [series, columns, rows, stackedSeries]; -const names = ['series', 'columns', 'rows', 'stackedSeries']; - -dateHistogramArray.forEach(function(data, i) { - describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function() { - const events = ['click', 'brush']; - let vis; - - beforeEach(() => { - vis = getVis(); - vis.render(data, getMockUiState()); - }); - - afterEach(function() { - vis.destroy(); - }); - - describe('render Method', function() { - it('should render charts', function() { - expect(vis.handler.charts.length).to.be.greaterThan(0); - vis.handler.charts.forEach(function(chart) { - expect($(chart.chartEl).find('svg').length).to.be(1); - }); - }); - }); - - describe('enable Method', function() { - let charts; - - beforeEach(function() { - charts = vis.handler.charts; - - charts.forEach(function(chart) { - events.forEach(function(event) { - vis.handler.enable(event, chart); - }); - }); - }); - - it('should add events to chart and emit to the Events class', function() { - charts.forEach(function(chart) { - events.forEach(function(event) { - expect(chart.events.listenerCount(event)).to.be.above(0); - }); - }); - }); - }); - - describe('disable Method', function() { - let charts; - - beforeEach(function() { - charts = vis.handler.charts; - - charts.forEach(function(chart) { - events.forEach(function(event) { - vis.handler.disable(event, chart); - }); - }); - }); - - it('should remove events from the chart', function() { - charts.forEach(function(chart) { - events.forEach(function(event) { - expect(chart.events.listenerCount(event)).to.be(0); - }); - }); - }); - }); - - describe('removeAll Method', function() { - beforeEach(function() { - vis.handler.removeAll(vis.element); - }); - - it('should remove all DOM elements from the el', function() { - expect($(vis.element).children().length).to.be(0); - }); - }); - - describe('error Method', function() { - beforeEach(function() { - vis.handler.error('This is an error!'); - }); - - it('should return an error classed DOM element with a text message', function() { - expect($(vis.element).find('.error').length).to.be(1); - expect($('.error h4').html()).to.be('This is an error!'); - }); - }); - - describe('destroy Method', function() { - beforeEach(function() { - vis.handler.destroy(); - }); - - it('should destroy all the charts in the visualization', function() { - expect(vis.handler.charts.length).to.be(0); - }); - }); - - describe('event proxying', function() { - it('should only pass the original event object to downstream handlers', function(done) { - const event = {}; - const chart = vis.handler.charts[0]; - - const mockEmitter = function() { - const args = Array.from(arguments); - expect(args.length).to.be(2); - expect(args[0]).to.be('click'); - expect(args[1]).to.be(event); - done(); - }; - - vis.emit = mockEmitter; - vis.handler.enable('click', chart); - chart.events.emit('click', event); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js deleted file mode 100644 index cc6d33a2d98da..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { layoutTypes as layoutType } from '../../../lib/layout/layout_types'; - -describe('Vislib Layout Types Test Suite', function() { - let layoutFunc; - - beforeEach(() => { - layoutFunc = layoutType.point_series; - }); - - it('should be an object', function() { - expect(_.isObject(layoutType)).to.be(true); - }); - - it('should return a function', function() { - expect(typeof layoutFunc).to.be('function'); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js deleted file mode 100644 index 3942aa18891b8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { chartSplit } from '../../../../../lib/layout/splits/column_chart/chart_split'; -import { chartTitleSplit } from '../../../../../lib/layout/splits/column_chart/chart_title_split'; -import { xAxisSplit } from '../../../../../lib/layout/splits/column_chart/x_axis_split'; -import { yAxisSplit } from '../../../../../lib/layout/splits/column_chart/y_axis_split'; - -describe('Vislib Split Function Test Suite', function() { - describe('Column Chart', function() { - let el; - const data = { - rows: [ - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - ], - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }); - - afterEach(function() { - el.remove(); - }); - - describe('chart split function', function() { - let fixture; - - beforeEach(function() { - fixture = d3.select('.visualization').call(chartSplit); - }); - - afterEach(function() { - fixture.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart').length).to.be(2); - }); - - it('should add the correct class name', function() { - expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); - }); - }); - - describe('chart title split function', function() { - let visEl; - let newEl; - let fixture; - - beforeEach(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - - newEl = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .datum({ series: [] }); - - newEl.append('div').attr('class', 'visAxis__splitTitles--x'); - newEl.append('div').attr('class', 'visAxis__splitTitles--y'); - newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - - fixture = newEl.selectAll(this.childNodes)[0].length; - }); - - afterEach(function() { - newEl.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart-title').length).to.be(2); - }); - - it('should remove the correct div', function() { - expect($('.visAxis__splitTitles--y').length).to.be(1); - expect($('.visAxis__splitTitles--x').length).to.be(0); - }); - - it('should remove all chart title divs when only one chart is rendered', function() { - expect(fixture).to.be(0); - }); - }); - - describe('x axis split function', function() { - let fixture; - let divs; - - beforeEach(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'columns') - .datum({ columns: [{}, {}] }); - d3.select('.columns').call(xAxisSplit); - divs = d3.selectAll('.x-axis-div')[0]; - }); - - afterEach(function() { - fixture.remove(); - $(divs).remove(); - }); - - it('should append the correct number of divs', function() { - expect(divs.length).to.be(2); - }); - }); - - describe('y axis split function', function() { - let fixture; - let divs; - - beforeEach(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'rows') - .datum({ rows: [{}, {}] }); - - d3.select('.rows').call(yAxisSplit); - - divs = d3.selectAll('.y-axis-div')[0]; - }); - - afterEach(function() { - fixture.remove(); - $(divs).remove(); - }); - - it('should append the correct number of divs', function() { - expect(divs.length).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js deleted file mode 100644 index 8978f80f58dde..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { chartSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_split'; -import { chartTitleSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_title_split'; - -describe('Vislib Gauge Split Function Test Suite', function() { - describe('Column Chart', function() { - let el; - const data = { - rows: [ - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - ], - }; - - beforeEach(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }); - - afterEach(function() { - el.remove(); - }); - - describe('chart split function', function() { - let fixture; - - beforeEach(function() { - fixture = d3.select('.visualization').call(chartSplit); - }); - - afterEach(function() { - fixture.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart').length).to.be(2); - }); - - it('should add the correct class name', function() { - expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); - }); - }); - - describe('chart title split function', function() { - let visEl; - - beforeEach(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - }); - - afterEach(function() { - visEl.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.visAxis__splitTitles--x .chart-title').length).to.be(2); - expect($('.visAxis__splitTitles--y .chart-title').length).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js deleted file mode 100644 index e9c2ff0d2fa07..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { layoutTypes } from '../../../../lib/layout/layout_types'; - -describe('Vislib Column Layout Test Suite', function() { - let columnLayout; - let el; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization'); - columnLayout = layoutTypes.point_series(el, data); - }); - - afterEach(function() { - el.remove(); - }); - - it('should return an array of objects', function() { - expect(Array.isArray(columnLayout)).to.be(true); - expect(_.isObject(columnLayout[0])).to.be(true); - }); - - it('should throw an error when the wrong number or no arguments provided', function() { - expect(function() { - layoutTypes.point_series(el); - }).to.throwError(); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js deleted file mode 100644 index 03646d08298dd..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import percentileTestdata from './testdata_linechart_percentile.json'; -import percentileTestdataResult from './testdata_linechart_percentile_result.json'; - -import { vislibPointSeriesTypes as pointSeriesConfig } from '../../../lib/types/point_series'; - -describe('Point Series Config Type Class Test Suite', function() { - let parsedConfig; - const histogramConfig = { - type: 'histogram', - addLegend: true, - tooltip: { - show: true, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - type: 'value', - labels: {}, - title: {}, - }, - ], - }; - - const data = { - get: prop => { - return data[prop] || data.data[prop] || null; - }, - getLabels: () => [], - data: { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'series', - yAxisFormatter: () => 'test', - }, - }; - - describe('histogram chart', function() { - beforeEach(function() { - parsedConfig = pointSeriesConfig.column(histogramConfig, data); - }); - it('should not throw an error when more than 25 series are provided', function() { - expect(parsedConfig.error).to.be.undefined; - }); - - it('should set axis title and formatter from data', () => { - expect(parsedConfig.categoryAxes[0].title.text).to.equal(data.data.xAxisLabel); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).to.not.be.undefined; - }); - }); - - describe('line chart', function() { - beforeEach(function() { - const percentileDataObj = { - get: prop => { - return data[prop] || data.data[prop] || null; - }, - getLabels: () => [], - data: percentileTestdata.data, - }; - parsedConfig = pointSeriesConfig.line(percentileTestdata.cfg, percentileDataObj); - }); - it('should render a percentile line chart', function() { - expect(JSON.stringify(parsedConfig)).to.eql(JSON.stringify(percentileTestdataResult)); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js deleted file mode 100644 index 7dfd2ded36a66..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import expect from '@kbn/expect'; - -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib VisConfig Class Test Suite', function() { - let el; - let visConfig; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .node(); - - visConfig = new VisConfig( - { - type: 'point_series', - }, - data, - getMockUiState(), - el, - () => undefined - ); - }); - - afterEach(() => { - el.remove(); - }); - - describe('get Method', function() { - it('should be a function', function() { - expect(typeof visConfig.get).to.be('function'); - }); - - it('should get the property', function() { - expect(visConfig.get('el')).to.be(el); - expect(visConfig.get('type')).to.be('point_series'); - }); - - it('should return defaults if property does not exist', function() { - expect(visConfig.get('this.does.not.exist', 'defaults')).to.be('defaults'); - }); - - it('should throw an error if property does not exist and defaults were not provided', function() { - expect(function() { - visConfig.get('this.does.not.exist'); - }).to.throwError(); - }); - }); - - describe('set Method', function() { - it('should be a function', function() { - expect(typeof visConfig.set).to.be('function'); - }); - - it('should set a property', function() { - visConfig.set('this.does.not.exist', 'it.does.now'); - expect(visConfig.get('this.does.not.exist')).to.be('it.does.now'); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js deleted file mode 100644 index d42562a87b825..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { Axis } from '../../lib/axis'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib xAxis Class Test Suite', function() { - let mockUiState; - let xAxis; - let el; - let fixture; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - xAxisOrderedValues: [ - 1408734060000, - 1408734090000, - 1408734120000, - 1408734150000, - 1408734180000, - 1408734210000, - 1408734240000, - 1408734270000, - 1408734300000, - 1408734330000, - ], - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisFormatter: function(thing) { - return new Date(thing); - }, - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - mockUiState = getMockUiState(); - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); - - fixture = el.append('div').attr('class', 'x-axis-div'); - - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - $('.x-axis-div')[0], - () => undefined - ); - xAxis = new Axis(visConfig, { - type: 'category', - id: 'CategoryAxis-1', - }); - }); - - afterEach(function() { - fixture.remove(); - el.remove(); - }); - - describe('render Method', function() { - beforeEach(function() { - xAxis.render(); - }); - - it('should append an svg to div', function() { - expect(el.selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect(el.selectAll('svg').select('g').length).to.be(1); - }); - - it('should append ticks with text', function() { - expect(!!el.selectAll('svg').selectAll('.tick text')).to.be(true); - }); - }); - - describe('getScale, getDomain, getTimeDomain, and getRange Methods', function() { - let timeScale; - let width; - let range; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - timeScale = xAxis.getScale(); - range = xAxis.axisScale.getRange(width); - }); - - it('should return a function', function() { - expect(_.isFunction(timeScale)).to.be(true); - }); - - it('should return the correct domain', function() { - expect(_.isDate(timeScale.domain()[0])).to.be(true); - expect(_.isDate(timeScale.domain()[1])).to.be(true); - }); - - it('should return the min and max dates', function() { - expect(timeScale.domain()[0].toDateString()).to.be(new Date(1408734060000).toDateString()); - expect(timeScale.domain()[1].toDateString()).to.be(new Date(1408734330000).toDateString()); - }); - - it('should return the correct range', function() { - expect(range[0]).to.be(0); - expect(range[1]).to.be(width); - }); - }); - - describe('getOrdinalDomain Method', function() { - let ordinalScale; - let ordinalDomain; - let width; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.ordered = null; - xAxis.axisConfig.ordered = null; - xAxis.getAxis(width); - ordinalScale = xAxis.getScale(); - ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); - }); - - it('should return an ordinal scale', function() { - expect(ordinalDomain.domain()[0]).to.be('this'); - expect(ordinalDomain.domain()[4]).to.be('array'); - }); - - it('should return an array of values', function() { - expect(Array.isArray(ordinalDomain.domain())).to.be(true); - }); - }); - - describe('getXScale Method', function() { - let width; - let xScale; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - xScale = xAxis.getScale(); - }); - - it('should return a function', function() { - expect(_.isFunction(xScale)).to.be(true); - }); - - it('should return a domain', function() { - expect(_.isDate(xScale.domain()[0])).to.be(true); - expect(_.isDate(xScale.domain()[1])).to.be(true); - }); - - it('should return a range', function() { - expect(xScale.range()[0]).to.be(0); - expect(xScale.range()[1]).to.be(width); - }); - }); - - describe('getXAxis Method', function() { - let width; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - }); - - it('should create an getScale function on the xAxis class', function() { - expect(_.isFunction(xAxis.getScale())).to.be(true); - }); - }); - - describe('draw Method', function() { - it('should be a function', function() { - expect(_.isFunction(xAxis.draw())).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js deleted file mode 100644 index f73011d661645..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { Axis } from '../../lib/axis'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -const YAxis = Axis; -let mockUiState; -let el; -let buildYAxis; -let yAxis; -let yAxisDiv; - -const timeSeries = [ - 1408734060000, - 1408734090000, - 1408734120000, - 1408734150000, - 1408734180000, - 1408734210000, - 1408734240000, - 1408734270000, - 1408734300000, - 1408734330000, -]; - -const defaultGraphData = [ - [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], - [2, 13, 20, 18, 26, 20, 16, 12, 19, 14], -]; - -function makeSeriesData(data) { - return timeSeries.map(function(timestamp, i) { - return { - x: timestamp, - y: data[i] || 0, - }; - }); -} - -function createData(seriesData) { - const data = { - hits: 621, - label: 'test', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: seriesData.map(function(series) { - return { values: makeSeriesData(series) }; - }), - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - const node = $('
') - .css({ - height: 40, - width: 40, - }) - .appendTo('body') - .addClass('y-axis-wrapper') - .get(0); - - el = d3.select(node).datum(data); - - yAxisDiv = el.append('div').attr('class', 'y-axis-div'); - - buildYAxis = function(params) { - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - node, - () => undefined - ); - return new YAxis( - visConfig, - _.merge( - {}, - { - id: 'ValueAxis-1', - type: 'value', - scale: { - defaultYMin: true, - setYExtents: false, - }, - }, - params - ) - ); - }; - - yAxis = buildYAxis(); -} - -describe('Vislib yAxis Class Test Suite', function() { - beforeEach(() => { - mockUiState = getMockUiState(); - expect($('.y-axis-wrapper')).to.have.length(0); - }); - - afterEach(function() { - if (el) { - el.remove(); - yAxisDiv.remove(); - } - }); - - describe('render Method', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.render(); - }); - - it('should append an svg to div', function() { - expect(el.selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect(el.selectAll('svg').select('g').length).to.be(1); - }); - - it('should append ticks with text', function() { - expect(!!el.selectAll('svg').selectAll('.tick text')).to.be(true); - }); - }); - - describe('getYScale Method', function() { - let yScale; - let graphData; - let domain; - const height = 50; - - function checkDomain(min, max) { - const domain = yScale.domain(); - expect(domain[0]).to.be.lessThan(min + 1); - expect(domain[1]).to.be.greaterThan(max - 1); - return domain; - } - - function checkRange() { - expect(yScale.range()[0]).to.be(height); - expect(yScale.range()[1]).to.be(0); - } - - describe('API', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should return a function', function() { - expect(_.isFunction(yScale)).to.be(true); - }); - }); - - describe('positive values', function() { - beforeEach(function() { - graphData = defaultGraphData; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between 0 and max value', function() { - const min = 0; - const max = _.max(_.flattenDeep(graphData)); - const domain = checkDomain(min, max); - expect(domain[1]).to.be.greaterThan(0); - checkRange(); - }); - }); - - describe('negative values', function() { - beforeEach(function() { - graphData = [ - [-8, -23, -30, -28, -36, -30, -26, -22, -29, -24], - [-22, -8, -30, -4, 0, 0, -3, -22, -14, -24], - ]; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between min value and 0', function() { - const min = _.min(_.flattenDeep(graphData)); - const max = 0; - const domain = checkDomain(min, max); - expect(domain[0]).to.be.lessThan(0); - checkRange(); - }); - }); - - describe('positive and negative values', function() { - beforeEach(function() { - graphData = [ - [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], - [22, 8, -30, -4, 0, 0, 3, -22, 14, 24], - ]; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between min and max values', function() { - const min = _.min(_.flattenDeep(graphData)); - const max = _.max(_.flattenDeep(graphData)); - const domain = checkDomain(min, max); - expect(domain[0]).to.be.lessThan(0); - expect(domain[1]).to.be.greaterThan(0); - checkRange(); - }); - }); - - describe('validate user defined values', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.axisConfig.set('scale.stacked', true); - yAxis.axisConfig.set('scale.setYExtents', false); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should throw a NaN error', function() { - const min = 'Not a number'; - const max = 12; - - expect(function() { - yAxis.axisScale.validateUserExtents(min, max); - }).to.throwError(); - }); - - it('should return a decimal value', function() { - yAxis.axisConfig.set('scale.mode', 'percentage'); - yAxis.axisConfig.set('scale.setYExtents', true); - yAxis.getAxis(height); - domain = []; - domain[0] = 20; - domain[1] = 80; - const newDomain = yAxis.axisScale.validateUserExtents(domain); - - expect(newDomain[0]).to.be(domain[0] / 100); - expect(newDomain[1]).to.be(domain[1] / 100); - }); - - it('should return the user defined value', function() { - domain = [20, 50]; - const newDomain = yAxis.axisScale.validateUserExtents(domain); - - expect(newDomain[0]).to.be(domain[0]); - expect(newDomain[1]).to.be(domain[1]); - }); - }); - - describe('should throw an error when', function() { - it('min === max', function() { - const min = 12; - const max = 12; - - expect(function() { - yAxis.axisScale.validateAxisExtents(min, max); - }).to.throwError(); - }); - - it('min > max', function() { - const min = 30; - const max = 10; - - expect(function() { - yAxis.axisScale.validateAxisExtents(min, max); - }).to.throwError(); - }); - }); - }); - - describe('getScaleType method', function() { - const fnNames = ['linear', 'log', 'square root']; - - it('should return a function', function() { - fnNames.forEach(function(fnName) { - expect(yAxis.axisScale.getD3Scale(fnName)).to.be.a(Function); - }); - - // if no value is provided to the function, scale should default to a linear scale - expect(yAxis.axisScale.getD3Scale()).to.be.a(Function); - }); - - it('should throw an error if function name is undefined', function() { - expect(function() { - yAxis.axisScale.getD3Scale('square'); - }).to.throwError(); - }); - }); - - describe('_logDomain method', function() { - it('should throw an error', function() { - expect(function() { - yAxis.axisScale.logDomain(-10, -5); - }).to.throwError(); - expect(function() { - yAxis.axisScale.logDomain(-10, 5); - }).to.throwError(); - expect(function() { - yAxis.axisScale.logDomain(0, -5); - }).to.throwError(); - }); - - it('should return a yMin value of 1', function() { - const yMin = yAxis.axisScale.logDomain(0, 200)[0]; - expect(yMin).to.be(1); - }); - }); - - describe('getYAxis method', function() { - let yMax; - beforeEach(function() { - createData(defaultGraphData); - yMax = yAxis.yMax; - }); - - afterEach(function() { - yAxis.yMax = yMax; - yAxis = buildYAxis(); - }); - - it('should use decimal format for small values', function() { - yAxis.yMax = 1; - const tickFormat = yAxis.getAxis().tickFormat(); - expect(tickFormat(0.8)).to.be('0.8'); - }); - }); - - describe('draw Method', function() { - beforeEach(function() { - createData(defaultGraphData); - }); - - it('should be a function', function() { - expect(_.isFunction(yAxis.draw())).to.be(true); - }); - }); - - describe('tickScale Method', function() { - beforeEach(function() { - createData(defaultGraphData); - }); - - it('should return the correct number of ticks', function() { - expect(yAxis.tickScale(1000)).to.be(11); - expect(yAxis.tickScale(40)).to.be(3); - expect(yAxis.tickScale(20)).to.be(0); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js deleted file mode 100644 index f4c952be191de..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import d3 from 'd3'; -import expect from '@kbn/expect'; - -// Data -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; -import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; - -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; - -// tuple, with the format [description, mode, data] -const dataTypesArray = [ - ['series', series], - ['series with positive and negative values', seriesPosNeg], - ['series with negative values', seriesNeg], - ['terms columns', termsColumns], - ['stackedSeries', stackedSeries], -]; - -describe('Vislib Heatmap Chart Test Suite', function() { - dataTypesArray.forEach(function(dataType) { - const name = dataType[0]; - const data = dataType[1]; - - describe('for ' + name + ' Data', function() { - let vis; - let mockUiState; - const visLibParams = { - type: 'heatmap', - addLegend: true, - addTooltip: true, - colorsNumber: 4, - colorSchema: 'Greens', - setColorRange: false, - percentageMode: true, - invertColors: false, - colorsRange: [], - }; - - function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); - vis = getVis(config); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(data, mockUiState); - } - - beforeEach(() => { - generateVis(); - }); - - afterEach(function() { - vis.destroy(); - }); - - it('category axes should be rendered in reverse order', () => { - const renderedCategoryAxes = vis.handler.renderArray.filter(item => { - return ( - item.constructor && - item.constructor.name === 'Axis' && - item.axisConfig.get('type') === 'category' - ); - }); - expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length); - expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal( - renderedCategoryAxes[1].axisConfig.get('id') - ); - expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal( - renderedCategoryAxes[0].axisConfig.get('id') - ); - }); - - describe('addSquares method', function() { - it('should append rects', function() { - vis.handler.charts.forEach(function(chart) { - const numOfRects = chart.chartData.series.reduce((result, series) => { - return result + series.values.length; - }, 0); - expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects); - }); - }); - }); - - describe('addBarEvents method', function() { - function checkChart(chart) { - const rect = $(chart.chartEl) - .find('.series rect') - .get(0); - - return { - click: !!rect.__onclick, - mouseOver: !!rect.__onmouseover, - // D3 brushing requires that a g element is appended that - // listens for mousedown events. This g element includes - // listeners, however, I was not able to test for the listener - // function being present. I will need to update this test - // in the future. - brush: !!d3.select('.brush')[0][0], - }; - } - - it('should attach the brush if data is a set of ordered dates', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - const ordered = vis.handler.data.get('ordered'); - const date = Boolean(ordered && ordered.date); - expect(has.brush).to.be(date); - }); - }); - - it('should attach a click event', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - expect(has.click).to.be(true); - }); - }); - - it('should attach a hover event', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - expect(has.mouseOver).to.be(true); - }); - }); - }); - - describe('draw method', function() { - it('should return a function', function() { - vis.handler.charts.forEach(function(chart) { - expect(_.isFunction(chart.draw())).to.be(true); - }); - }); - - it('should return a yMin and yMax', function() { - vis.handler.charts.forEach(function(chart) { - const yAxis = chart.handler.valueAxes[0]; - const domain = yAxis.getScale().domain(); - - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); - }); - }); - }); - - it('should define default colors', function() { - expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); - }); - - it('should set custom range', function() { - vis.destroy(); - generateVis({ - setColorRange: true, - colorsRange: [ - { from: 0, to: 200 }, - { from: 200, to: 400 }, - { from: 400, to: 500 }, - { from: 500, to: Infinity }, - ], - }); - const labels = vis.getLegendLabels(); - expect(labels[0]).to.be('0 - 200'); - expect(labels[1]).to.be('200 - 400'); - expect(labels[2]).to.be('400 - 500'); - expect(labels[3]).to.be('500 - Infinity'); - }); - - it('should show correct Y axis title', function() { - expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal(''); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js deleted file mode 100644 index d69f952325ed0..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import d3 from 'd3'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import terms from '../lib/fixtures/mock_data/terms/_columns'; -import { TimeMarker } from '../../visualizations/time_marker'; - -describe('Vislib Time Marker Test Suite', function() { - const height = 50; - const color = '#ff0000'; - const opacity = 0.5; - const width = 3; - const customClass = 'custom-time-marker'; - const dateMathTimes = ['now-1m', 'now-5m', 'now-15m']; - const myTimes = dateMathTimes.map(function(dateMathString) { - return { - time: dateMathString, - class: customClass, - color: color, - opacity: opacity, - width: width, - }; - }); - const getExtent = function(dataArray, func) { - return func(dataArray, function(obj) { - return func(obj.values, function(d) { - return d.x; - }); - }); - }; - const times = []; - let defaultMarker; - let customMarker; - let selection; - let xScale; - let minDomain; - let maxDomain; - let domain; - - beforeEach(function() { - minDomain = getExtent(series.series, d3.min); - maxDomain = getExtent(series.series, d3.max); - domain = [minDomain, maxDomain]; - xScale = d3.time - .scale() - .domain(domain) - .range([0, 500]); - defaultMarker = new TimeMarker(times, xScale, height); - customMarker = new TimeMarker(myTimes, xScale, height); - - selection = d3 - .select('body') - .append('div') - .attr('class', 'marker'); - selection.datum(series); - }); - - afterEach(function() { - selection.remove('*'); - selection = null; - defaultMarker = null; - }); - - describe('_isTimeBaseChart method', function() { - let boolean; - let newSelection; - - it('should return true when data is time based', function() { - boolean = defaultMarker._isTimeBasedChart(selection); - expect(boolean).to.be(true); - }); - - it('should return false when data is not time based', function() { - newSelection = selection.datum(terms); - boolean = defaultMarker._isTimeBasedChart(newSelection); - expect(boolean).to.be(false); - }); - }); - - describe('render method', function() { - let lineArray; - - beforeEach(function() { - defaultMarker.render(selection); - customMarker.render(selection); - lineArray = document.getElementsByClassName('custom-time-marker'); - }); - - it('should render the default line', function() { - expect(!!$('line.time-marker').length).to.be(true); - }); - - it('should render the custom (user defined) lines', function() { - expect($('line.custom-time-marker').length).to.be(myTimes.length); - }); - - it('should set the class', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(line.getAttribute('class')).to.be(customClass); - }); - }); - - it('should set the stroke', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(line.getAttribute('stroke')).to.be(color); - }); - }); - - it('should set the stroke-opacity', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(+line.getAttribute('stroke-opacity')).to.be(opacity); - }); - }); - - it('should set the stroke-width', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(+line.getAttribute('stroke-width')).to.be(width); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js deleted file mode 100644 index c8f0faf8dcca5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { visTypes } from '../../visualizations/vis_types'; - -describe('Vislib Vis Types Test Suite', function() { - let visFunc; - - beforeEach(function() { - visFunc = visTypes.point_series; - }); - - it('should be an object', function() { - expect(_.isObject(visTypes)).to.be(true); - }); - - it('should return a function', function() { - expect(typeof visFunc).to.be('function'); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js deleted file mode 100644 index 129006cdf0ca3..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js +++ /dev/null @@ -1,510 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { injectZeros } from './inject_zeros'; -import { orderXValues } from './ordered_x_keys'; -import { getUniqKeys } from './uniq_keys'; -import { flattenData } from './flatten_data'; -import { createZeroFilledArray } from './zero_filled_array'; -import { zeroFillDataArray } from './zero_fill_data_array'; - -describe('Vislib Zero Injection Module Test Suite', function() { - const dateHistogramRowsObj = { - xAxisOrderedValues: [ - 1418410560000, - 1418410620000, - 1418410680000, - 1418410740000, - 1418410800000, - 1418410860000, - 1418410920000, - ], - series: [ - { - label: 'html', - values: [ - { x: 1418410560000, y: 2 }, - { x: 1418410620000, y: 4 }, - { x: 1418410680000, y: 1 }, - { x: 1418410740000, y: 5 }, - { x: 1418410800000, y: 2 }, - { x: 1418410860000, y: 3 }, - { x: 1418410920000, y: 2 }, - ], - }, - { - label: 'css', - values: [ - { x: 1418410560000, y: 1 }, - { x: 1418410620000, y: 3 }, - { x: 1418410680000, y: 1 }, - { x: 1418410740000, y: 4 }, - { x: 1418410800000, y: 2 }, - ], - }, - ], - }; - const dateHistogramRows = dateHistogramRowsObj.series; - - const seriesDataObj = { - xAxisOrderedValues: ['v1', 'v2', 'v3', 'v4', 'v5'], - series: [ - { - label: '200', - values: [ - { x: 'v1', y: 234 }, - { x: 'v2', y: 34 }, - { x: 'v3', y: 834 }, - { x: 'v4', y: 1234 }, - { x: 'v5', y: 4 }, - ], - }, - ], - }; - const seriesData = seriesDataObj.series; - - const multiSeriesDataObj = { - xAxisOrderedValues: ['1', '2', '3', '4', '5'], - series: [ - { - label: '200', - values: [ - { x: '1', y: 234 }, - { x: '2', y: 34 }, - { x: '3', y: 834 }, - { x: '4', y: 1234 }, - { x: '5', y: 4 }, - ], - }, - { - label: '404', - values: [ - { x: '1', y: 1234 }, - { x: '3', y: 234 }, - { x: '5', y: 34 }, - ], - }, - { - label: '503', - values: [{ x: '3', y: 834 }], - }, - ], - }; - const multiSeriesData = multiSeriesDataObj.series; - - const multiSeriesNumberedDataObj = { - xAxisOrderedValues: [1, 2, 3, 4, 5], - series: [ - { - label: '200', - values: [ - { x: 1, y: 234 }, - { x: 2, y: 34 }, - { x: 3, y: 834 }, - { x: 4, y: 1234 }, - { x: 5, y: 4 }, - ], - }, - { - label: '404', - values: [ - { x: 1, y: 1234 }, - { x: 3, y: 234 }, - { x: 5, y: 34 }, - ], - }, - { - label: '503', - values: [{ x: 3, y: 834 }], - }, - ], - }; - const multiSeriesNumberedData = multiSeriesNumberedDataObj.series; - - const emptyObject = {}; - const str = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const emptyArray = []; - let notAValue; - - describe('Zero Injection (main)', function() { - let sample1; - let sample2; - let sample3; - - beforeEach(() => { - sample1 = injectZeros(seriesData, seriesDataObj); - sample2 = injectZeros(multiSeriesData, multiSeriesDataObj); - sample3 = injectZeros(multiSeriesNumberedData, multiSeriesNumberedDataObj); - }); - - it('should be a function', function() { - expect(_.isFunction(injectZeros)).to.be(true); - }); - - it('should return an object with series[0].values', function() { - expect(_.isObject(sample1)).to.be(true); - expect(_.isObject(sample1[0].values)).to.be(true); - }); - - it('should return the same array of objects when the length of the series array is 1', function() { - expect(sample1[0].values[0].x).to.be(seriesData[0].values[0].x); - expect(sample1[0].values[1].x).to.be(seriesData[0].values[1].x); - expect(sample1[0].values[2].x).to.be(seriesData[0].values[2].x); - expect(sample1[0].values[3].x).to.be(seriesData[0].values[3].x); - expect(sample1[0].values[4].x).to.be(seriesData[0].values[4].x); - }); - - it('should inject zeros in the input array', function() { - expect(sample2[1].values[1].y).to.be(0); - expect(sample2[2].values[0].y).to.be(0); - expect(sample2[2].values[1].y).to.be(0); - expect(sample2[2].values[4].y).to.be(0); - expect(sample3[1].values[1].y).to.be(0); - expect(sample3[2].values[0].y).to.be(0); - expect(sample3[2].values[1].y).to.be(0); - expect(sample3[2].values[4].y).to.be(0); - }); - - it('should return values arrays with the same x values', function() { - expect(sample2[1].values[0].x).to.be(sample2[2].values[0].x); - expect(sample2[1].values[1].x).to.be(sample2[2].values[1].x); - expect(sample2[1].values[2].x).to.be(sample2[2].values[2].x); - expect(sample2[1].values[3].x).to.be(sample2[2].values[3].x); - expect(sample2[1].values[4].x).to.be(sample2[2].values[4].x); - }); - - it('should return values arrays of the same length', function() { - expect(sample2[0].values.length).to.be(sample2[1].values.length); - expect(sample2[0].values.length).to.be(sample2[2].values.length); - expect(sample2[1].values.length).to.be(sample2[2].values.length); - }); - }); - - describe('Order X Values', function() { - let results; - let numberedResults; - - beforeEach(() => { - results = orderXValues(multiSeriesDataObj); - numberedResults = orderXValues(multiSeriesNumberedDataObj); - }); - - it('should return a function', function() { - expect(_.isFunction(orderXValues)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of values ordered by their index by default', function() { - expect(results[0]).to.be('1'); - expect(results[1]).to.be('2'); - expect(results[2]).to.be('3'); - expect(results[3]).to.be('4'); - expect(results[4]).to.be('5'); - expect(numberedResults[0]).to.be(1); - expect(numberedResults[1]).to.be(2); - expect(numberedResults[2]).to.be(3); - expect(numberedResults[3]).to.be(4); - expect(numberedResults[4]).to.be(5); - }); - - it('should return an array of values that preserve the index from xAxisOrderedValues', function() { - const data = { - xAxisOrderedValues: ['1', '2', '3', '4', '5'], - series: [ - { - label: '200', - values: [ - { x: '2', y: 34 }, - { x: '4', y: 1234 }, - ], - }, - { - label: '404', - values: [ - { x: '1', y: 1234 }, - { x: '3', y: 234 }, - { x: '5', y: 34 }, - ], - }, - { - label: '503', - values: [{ x: '3', y: 834 }], - }, - ], - }; - const result = orderXValues(data); - expect(result).to.eql(['1', '2', '3', '4', '5']); - }); - - it('should return an array of values ordered by their sum when orderBucketsBySum is true', function() { - const orderBucketsBySum = true; - results = orderXValues(multiSeriesDataObj, orderBucketsBySum); - numberedResults = orderXValues(multiSeriesNumberedDataObj, orderBucketsBySum); - - expect(results[0]).to.be('3'); - expect(results[1]).to.be('1'); - expect(results[2]).to.be('4'); - expect(results[3]).to.be('5'); - expect(results[4]).to.be('2'); - expect(numberedResults[0]).to.be(3); - expect(numberedResults[1]).to.be(1); - expect(numberedResults[2]).to.be(4); - expect(numberedResults[3]).to.be(5); - expect(numberedResults[4]).to.be(2); - }); - }); - - describe('Unique Keys', function() { - let results; - - beforeEach(() => { - results = getUniqKeys(multiSeriesDataObj); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - getUniqKeys(str); - }).to.throwError(); - - expect(function() { - getUniqKeys(number); - }).to.throwError(); - - expect(function() { - getUniqKeys(boolean); - }).to.throwError(); - - expect(function() { - getUniqKeys(nullValue); - }).to.throwError(); - - expect(function() { - getUniqKeys(emptyArray); - }).to.throwError(); - - expect(function() { - getUniqKeys(notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(getUniqKeys)).to.be(true); - }); - - it('should return an object', function() { - expect(_.isObject(results)).to.be(true); - }); - - it('should return an object of unique keys', function() { - expect(_.uniq(_.keys(results)).length).to.be(_.keys(results).length); - }); - }); - - describe('Flatten Data', function() { - let results; - - beforeEach(() => { - results = flattenData(multiSeriesDataObj); - }); - - it('should return a function', function() { - expect(_.isFunction(flattenData)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results[0])).to.be(true); - expect(_.isObject(results[1])).to.be(true); - expect(_.isObject(results[2])).to.be(true); - }); - }); - - describe('Zero Filled Array', function() { - const arr1 = [1, 2, 3, 4, 5]; - const arr2 = ['1', '2', '3', '4', '5']; - let results1; - let results2; - - beforeEach(() => { - results1 = createZeroFilledArray(arr1); - results2 = createZeroFilledArray(arr2); - }); - - it('should throw an error if input is not an array', function() { - expect(function() { - createZeroFilledArray(str); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(number); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(boolean); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(nullValue); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(emptyObject); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(createZeroFilledArray)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results1)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results1[0])).to.be(true); - expect(_.isObject(results1[1])).to.be(true); - expect(_.isObject(results1[2])).to.be(true); - expect(_.isObject(results1[3])).to.be(true); - expect(_.isObject(results1[4])).to.be(true); - }); - - it('should return an array of objects where each y value is 0', function() { - expect(results1[0].y).to.be(0); - expect(results1[1].y).to.be(0); - expect(results1[2].y).to.be(0); - expect(results1[3].y).to.be(0); - expect(results1[4].y).to.be(0); - }); - - it('should return an array of objects where each x values are numbers', function() { - expect(_.isNumber(results1[0].x)).to.be(true); - expect(_.isNumber(results1[1].x)).to.be(true); - expect(_.isNumber(results1[2].x)).to.be(true); - expect(_.isNumber(results1[3].x)).to.be(true); - expect(_.isNumber(results1[4].x)).to.be(true); - }); - - it('should return an array of objects where each x values are strings', function() { - expect(_.isString(results2[0].x)).to.be(true); - expect(_.isString(results2[1].x)).to.be(true); - expect(_.isString(results2[2].x)).to.be(true); - expect(_.isString(results2[3].x)).to.be(true); - expect(_.isString(results2[4].x)).to.be(true); - }); - }); - - describe('Zero Filled Data Array', function() { - const xValueArr = [1, 2, 3, 4, 5]; - let arr1; - const arr2 = [{ x: 3, y: 834 }]; - let results; - - beforeEach(() => { - arr1 = createZeroFilledArray(xValueArr); - // Takes zero array as 1st arg and data array as 2nd arg - results = zeroFillDataArray(arr1, arr2); - }); - - it('should throw an error if input are not arrays', function() { - expect(function() { - zeroFillDataArray(str, str); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(number, number); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(boolean, boolean); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(nullValue, nullValue); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(emptyObject, emptyObject); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(notAValue, notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(zeroFillDataArray)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results[0])).to.be(true); - expect(_.isObject(results[1])).to.be(true); - expect(_.isObject(results[2])).to.be(true); - }); - - it('should return an array with zeros injected in the appropriate objects as y values', function() { - expect(results[0].y).to.be(0); - expect(results[1].y).to.be(0); - expect(results[3].y).to.be(0); - expect(results[4].y).to.be(0); - }); - }); - - describe('Injected Zero values return in the correct order', function() { - let results; - - beforeEach(() => { - results = injectZeros(dateHistogramRows, dateHistogramRowsObj); - }); - - it('should return an array of objects', function() { - results.forEach(function(row) { - expect(Array.isArray(row.values)).to.be(true); - }); - }); - - it('should return ordered x values', function() { - const values = results[0].values; - expect(values[0].x).to.be.lessThan(values[1].x); - expect(values[1].x).to.be.lessThan(values[2].x); - expect(values[2].x).to.be.lessThan(values[3].x); - expect(values[3].x).to.be.lessThan(values[4].x); - expect(values[4].x).to.be.lessThan(values[5].x); - expect(values[5].x).to.be.lessThan(values[6].x); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js deleted file mode 100644 index b2559c085aeab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { VislibProvider } from './vislib'; - -// eslint-disable-next-line import/no-default-export -export default VislibProvider; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js deleted file mode 100644 index f33ce0395af1f..0000000000000 --- 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 38a6be548594f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import stackedSeries from '../../__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series'; -import { vislibPointSeriesTypes } from './point_series'; - -describe('vislibPointSeriesTypes', () => { - const heatmapConfig = { - type: 'heatmap', - addLegend: true, - addTooltip: true, - colorsNumber: 4, - colorSchema: 'Greens', - setColorRange: false, - percentageMode: true, - invertColors: false, - colorsRange: [], - heatmapMaxBuckets: 20, - }; - - const stackedData = { - get: prop => { - return stackedSeries[prop] || null; - }, - getLabels: () => [], - data: stackedSeries, - }; - - const maxBucketData = { - get: prop => { - return maxBucketData[prop] || maxBucketData.data[prop] || null; - }, - getLabels: () => [], - data: { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'series', - yAxisFormatter: () => 'test', - }, - }; - - describe('axis formatters', () => { - it('should create a value axis config with the default y axis formatter', () => { - const parsedConfig = vislibPointSeriesTypes.line({}, maxBucketData); - expect(parsedConfig.valueAxes.length).toEqual(1); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe( - maxBucketData.data.yAxisFormatter - ); - }); - - it('should use the formatter of the first series matching the axis if there is a descriptor', () => { - const axisFormatter1 = jest.fn(); - const axisFormatter2 = jest.fn(); - const axisFormatter3 = jest.fn(); - const parsedConfig = vislibPointSeriesTypes.line( - { - valueAxes: [ - { - id: 'ValueAxis-1', - labels: {}, - }, - { - id: 'ValueAxis-2', - labels: {}, - }, - ], - seriesParams: [ - { - valueAxis: 'ValueAxis-1', - data: { - id: '2', - }, - }, - { - valueAxis: 'ValueAxis-2', - data: { - id: '3', - }, - }, - { - valueAxis: 'ValueAxis-2', - data: { - id: '4', - }, - }, - ], - }, - { - ...maxBucketData, - data: { - ...maxBucketData.data, - series: [ - { id: '2.1', label: 's1', values: [], yAxisFormatter: axisFormatter1 }, - { id: '2.2', label: 's2', values: [], yAxisFormatter: axisFormatter1 }, - { id: '3.1', label: 's3', values: [], yAxisFormatter: axisFormatter2 }, - { id: '3.2', label: 's4', values: [], yAxisFormatter: axisFormatter2 }, - { id: '4.1', label: 's5', values: [], yAxisFormatter: axisFormatter3 }, - { id: '4.2', label: 's6', values: [], yAxisFormatter: axisFormatter3 }, - ], - }, - } - ); - expect(parsedConfig.valueAxes.length).toEqual(2); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe(axisFormatter1); - expect(parsedConfig.valueAxes[1].labels.axisFormatter).toBe(axisFormatter2); - }); - }); - - describe('heatmap()', () => { - it('should return an error when more than 20 series are provided', () => { - const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, maxBucketData); - expect(parsedConfig.error).toMatchInlineSnapshot( - `"There are too many series defined (26). The configured maximum is 20."` - ); - }); - - it('should return valid config when less than 20 series are provided', () => { - const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, stackedData); - expect(parsedConfig.error).toBeUndefined(); - expect(parsedConfig.valueAxes[0].show).toBeFalsy(); - expect(parsedConfig.categoryAxes.length).toBe(2); - expect(parsedConfig.error).toBeUndefined(); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js deleted file mode 100644 index 024dee60ef2bf..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './lib/types/pie'; -import './lib/types/point_series'; -import './lib/types'; -import './lib/layout/layout_types'; -import './lib/data'; -import './visualizations/vis_types'; -import { Vis } from './vis'; - -// prefetched for faster optimization runs -// end prefetching - -/** - * Provides the Kibana4 Visualization Library - * - * @module vislib - * @main vislib - * @return {Object} Contains the version number and the Vis Class for creating visualizations - */ -export function VislibProvider() { - return { - version: '0.0.0', - Vis, - }; -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js deleted file mode 100644 index 9822932c6071b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import moment from 'moment'; - -import { isColorDark } from '@elastic/eui'; - -import { PointSeries } from './_point_series'; -import { getHeatmapColors } from '../../.../../../../../../../plugins/charts/public'; - -const defaults = { - color: undefined, // todo - fillColor: undefined, // todo -}; -/** - * Line Chart Visualization - * - * @class HeatmapChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ -export class HeatmapChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); - - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - - this.handler.visConfig.set('legend', { - labels: this.getHeatmapLabels(this.handler.visConfig), - colors: this.getHeatmapColors(this.handler.visConfig), - }); - - const colors = this.handler.visConfig.get('legend.colors', null); - if (colors) { - this.handler.vis.uiState.setSilent('vis.defaultColors', null); - this.handler.vis.uiState.setSilent('vis.defaultColors', colors); - } - } - - getHeatmapLabels(cfg) { - const percentageMode = cfg.get('percentageMode'); - const colorsNumber = cfg.get('colorsNumber'); - const colorsRange = cfg.get('colorsRange'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const zScale = this.getValueAxis().getScale(); - const [min, max] = zScale.domain(); - const labels = []; - const maxColorCnt = 10; - if (cfg.get('setColorRange')) { - colorsRange.forEach(range => { - const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; - const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; - labels.push(`${from} - ${to}`); - }); - } else { - if (max === min) { - return [min.toString()]; - } - for (let i = 0; i < colorsNumber; i++) { - let label; - let val = i / colorsNumber; - let nextVal = (i + 1) / colorsNumber; - if (percentageMode) { - val = Math.ceil(val * 100); - nextVal = Math.ceil(nextVal * 100); - label = `${val}% - ${nextVal}%`; - } else { - val = val * (max - min) + min; - nextVal = nextVal * (max - min) + min; - if (max - min > maxColorCnt) { - const valInt = Math.ceil(val); - if (i === 0) { - val = valInt === val ? val : valInt - 1; - } else { - val = valInt; - } - nextVal = Math.ceil(nextVal); - } - if (isFinite(val)) val = zAxisFormatter(val); - if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); - label = `${val} - ${nextVal}`; - } - - labels.push(label); - } - } - - return labels; - } - - getHeatmapColors(cfg) { - const invertColors = cfg.get('invertColors'); - const colorSchema = cfg.get('colorSchema'); - const labels = this.getHeatmapLabels(cfg); - const colors = {}; - for (const i in labels) { - if (labels[i]) { - const val = invertColors ? 1 - i / labels.length : i / labels.length; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - } - return colors; - } - - addSquares(svg, data) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.handler.categoryAxes[1].getScale(); - const zScale = this.getValueAxis().getScale(); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const colorsNumber = this.handler.visConfig.get('colorsNumber'); - const setColorRange = this.handler.visConfig.get('setColorRange'); - const colorsRange = this.handler.visConfig.get('colorsRange'); - const color = this.handler.data.getColorFunc(); - const labels = this.handler.visConfig.get('legend.labels'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const showLabels = zAxisConfig.get('labels.show'); - const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); - - const layer = svg.append('g').attr('class', 'series'); - - const squares = layer.selectAll('g.square').data(data.values); - - squares.exit().remove(); - - let barWidth; - if (this.getCategoryAxis().axisConfig.isTimeDomain()) { - const { min, interval } = this.handler.data.get('ordered'); - const start = min; - const end = moment(min) - .add(interval) - .valueOf(); - - barWidth = xScale(end) - xScale(start); - if (!isHorizontal) barWidth *= -1; - } - - function x(d) { - return xScale(d.x); - } - - function y(d) { - return yScale(d.series); - } - - const [min, max] = zScale.domain(); - function getColorBucket(d) { - let val = 0; - if (setColorRange && colorsRange.length) { - const bucket = _.find(colorsRange, range => { - return range.from <= d.y && range.to > d.y; - }); - return bucket ? colorsRange.indexOf(bucket) : -1; - } else { - if (isNaN(min) || isNaN(max)) { - val = colorsNumber - 1; - } else if (min === max) { - val = 0; - } else { - val = (d.y - min) / (max - min); /* get val from 0 - 1 */ - val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); - } - } - if (d.y == null) { - return -1; - } - return !isNaN(val) ? val : -1; - } - - function label(d) { - const colorBucket = getColorBucket(d); - // colorBucket id should always GTE 0 - if (colorBucket < 0) d.hide = true; - return labels[colorBucket]; - } - - function z(d) { - if (label(d) === '') return 'transparent'; - return color(label(d)); - } - - const squareWidth = barWidth || xScale.rangeBand(); - const squareHeight = yScale.rangeBand(); - - squares - .enter() - .append('g') - .attr('class', 'square'); - - squares - .append('rect') - .attr('x', x) - .attr('width', squareWidth) - .attr('y', y) - .attr('height', squareHeight) - .attr('data-label', label) - .attr('fill', z) - .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') - .style('display', d => { - return d.hide ? 'none' : 'initial'; - }); - - // todo: verify that longest label is not longer than the barwidth - // or barwidth is not smaller than textheight (and vice versa) - // - if (showLabels) { - const rotate = zAxisConfig.get('labels.rotate'); - const rotateRad = (rotate * Math.PI) / 180; - const cellPadding = 5; - const maxLength = - Math.min( - Math.abs(squareWidth / Math.cos(rotateRad)), - Math.abs(squareHeight / Math.sin(rotateRad)) - ) - cellPadding; - const maxHeight = - Math.min( - Math.abs(squareWidth / Math.sin(rotateRad)), - Math.abs(squareHeight / Math.cos(rotateRad)) - ) - cellPadding; - - let labelColor; - if (overwriteLabelColor) { - // If overwriteLabelColor is true, use the manual specified color - labelColor = zAxisConfig.get('labels.color'); - } else { - // Otherwise provide a function that will calculate a light or dark color - labelColor = d => { - const bgColor = z(d); - const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); - return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) - ? '#FFF' - : '#222'; - }; - } - - let hiddenLabels = false; - squares - .append('text') - .text(d => zAxisFormatter(d.y)) - .style('display', function(d) { - const textLength = this.getBBox().width; - const textHeight = this.getBBox().height; - const textTooLong = textLength > maxLength; - const textTooWide = textHeight > maxHeight; - if (!d.hide && (textTooLong || textTooWide)) { - hiddenLabels = true; - } - return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; - }) - .style('dominant-baseline', 'central') - .style('text-anchor', 'middle') - .style('fill', labelColor) - .attr('x', function(d) { - const center = x(d) + squareWidth / 2; - return center; - }) - .attr('y', function(d) { - const center = y(d) + squareHeight / 2; - return center; - }) - .attr('transform', function(d) { - const horizontalCenter = x(d) + squareWidth / 2; - const verticalCenter = y(d) + squareHeight / 2; - return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; - }); - if (hiddenLabels) { - this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); - } - } - - if (isTooltip) { - squares.call(tooltip.render()); - } - - return squares.selectAll('rect'); - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - - return function(selection) { - selection.each(function() { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const squares = self.addSquares(svg, self.chartData); - self.addCircleEvents(squares); - - self.events.emit('rendered', { - chart: self.chartData, - }); - - return svg; - }); - }; - } -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 0d2f3528c9019..02491ff872981 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -153,6 +153,7 @@ export default class KbnServer { public server: Server; public inject: Server['inject']; public pluginSpecs: any[]; + public uiBundles: any; constructor( settings: Record, diff --git a/src/legacy/ui/public/angular_ui_select.js b/src/legacy/ui/public/angular_ui_select.js index 92b1bf53520ce..99f92587507c9 100644 --- a/src/legacy/ui/public/angular_ui_select.js +++ b/src/legacy/ui/public/angular_ui_select.js @@ -19,6 +19,7 @@ import 'jquery'; import 'angular'; +// required for `ngSanitize` angular module import 'angular-sanitize'; import 'ui-select/dist/select'; diff --git a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap deleted file mode 100644 index 19d12f4bbbd4c..0000000000000 --- a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap +++ /dev/null @@ -1,1450 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldEditor should render create new scripted field correctly 1`] = ` -
- -

- -

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

- -

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

- -

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

- -

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

- -

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

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

extends PureComponent< + FormatEditorProps

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

- -

-

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

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

- -

- ' + - i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.idLabel', { - defaultMessage: 'User', - }) + - ' #1234', - }, - { - input: '/assets/main.css', - urlTemplate: 'http://site.com{{rawValue}}', - labelTemplate: i18n.translate( - 'common.ui.fieldEditor.labelTemplate.example.pathLabel', - { defaultMessage: 'View Asset' } - ), - output: - '' + - i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.pathLabel', { - defaultMessage: 'View Asset', - }) + - '', - }, - ]} - columns={[ - { - field: 'input', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.inputHeader', { - defaultMessage: 'Input', - }), - width: '160px', - }, - { - field: 'urlTemplate', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.urlHeader', { - defaultMessage: 'URL Template', - }), - }, - { - field: 'labelTemplate', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.labelHeader', { - defaultMessage: 'Label Template', - }), - }, - { - field: 'output', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.outputHeader', { - defaultMessage: 'Output', - }), - render: value => { - return ( - - ); - }, - }, - ]} - /> - - - - ) : null; -}; - -LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx new file mode 100644 index 0000000000000..1ce7bec579e16 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx @@ -0,0 +1,153 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface LabelTemplateExampleItem { + input: string | number; + urlTemplate: string; + labelTemplate: string; + output: string; +} + +const items: LabelTemplateExampleItem[] = [ + { + input: 1234, + urlTemplate: 'http://company.net/profiles?user_id={{value}}', + labelTemplate: i18n.translate('common.ui.fieldEditor.labelTemplate.example.idLabel', { + defaultMessage: 'User #{value}', + values: { value: '{{value}}' }, + }), + output: + '' + + i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.idLabel', { + defaultMessage: 'User', + }) + + ' #1234', + }, + { + input: '/assets/main.css', + urlTemplate: 'http://site.com{{rawValue}}', + labelTemplate: i18n.translate('common.ui.fieldEditor.labelTemplate.example.pathLabel', { + defaultMessage: 'View Asset', + }), + output: + '' + + i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.pathLabel', { + defaultMessage: 'View Asset', + }) + + '', + }, +]; + +export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { + return isVisible ? ( + + + +

+ +

+

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

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

+ +

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

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

-

- -

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

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

+

+ +

+
+ +
+`; + +exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.tsx diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/index.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/index.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/index.ts diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js deleted file mode 100644 index b810541d72697..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Fragment } from 'react'; -import { getDocLink } from 'ui/documentation_links'; - -import { EuiCallOut, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ScriptingWarningCallOut = ({ isVisible = false }) => { - return isVisible ? ( - - - } - color="warning" - iconType="alert" - > -

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

-

- -

-
- -
- ) : null; -}; - -ScriptingWarningCallOut.displayName = 'ScriptingWarningCallOut'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js deleted file mode 100644 index 48094f63f8fe8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { ScriptingWarningCallOut } from './warning_call_out'; - -jest.mock('ui/documentation_links', () => ({ - getDocLink: doc => `(docLink for ${doc})`, -})); - -describe('ScriptingWarningCallOut', () => { - it('should render normally', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if not visible', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx new file mode 100644 index 0000000000000..8568c2c79816b --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ScriptingWarningCallOut } from './warning_call_out'; +// eslint-disable-next-line +import { docLinksServiceMock } from '../../../../../../core/public/doc_links/doc_links_service.mock'; + +describe('ScriptingWarningCallOut', () => { + const docLinksScriptedFields = docLinksServiceMock.createStartContract().links.scriptedFields; + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if not visible', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx new file mode 100644 index 0000000000000..7dac6681fa1ea --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { EuiCallOut, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ScriptingWarningCallOutProps { + isVisible: boolean; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingWarningCallOut = ({ + isVisible = false, + docLinksScriptedFields, +}: ScriptingWarningCallOutProps) => { + return isVisible ? ( + + + } + color="warning" + iconType="alert" + > +

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

+

+ +

+
+ +
+ ) : null; +}; + +ScriptingWarningCallOut.displayName = 'ScriptingWarningCallOut'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap deleted file mode 100644 index ca252b6d0147b..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingHelpFlyout should render normally 1`] = ` - - - , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - } - } - tabs={ - Array [ - Object { - "content": , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - }, - Object { - "content": , - "data-test-subj": "testTab", - "id": "test", - "name": "Preview results", - }, - ] - } - /> - - -`; - -exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap new file mode 100644 index 0000000000000..282e8e311d984 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptingHelpFlyout should render normally 1`] = ` + + + , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + } + } + tabs={ + Array [ + Object { + "content": , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + }, + Object { + "content": , + "data-test-subj": "testTab", + "id": "test", + "name": "Preview results", + }, + ] + } + /> + + +`; + +exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = ` + + + , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + } + } + tabs={ + Array [ + Object { + "content": , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + }, + Object { + "content": , + "data-test-subj": "testTab", + "id": "test", + "name": "Preview results", + }, + ] + } + /> + + +`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js deleted file mode 100644 index c512b5f5f2019..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import { EuiFlyout, EuiFlyoutBody, EuiTabbedContent } from '@elastic/eui'; - -import { ScriptingSyntax } from './scripting_syntax'; -import { TestScript } from './test_script'; - -export const ScriptingHelpFlyout = ({ - isVisible = false, - onClose = () => {}, - indexPattern, - lang, - name, - script, - executeScript, -}) => { - const tabs = [ - { - id: 'syntax', - name: 'Syntax', - ['data-test-subj']: 'syntaxTab', - content: , - }, - { - id: 'test', - name: 'Preview results', - ['data-test-subj']: 'testTab', - content: ( - - ), - }, - ]; - - return isVisible ? ( - - - - - - ) : null; -}; - -ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; - -ScriptingHelpFlyout.propTypes = { - indexPattern: PropTypes.object.isRequired, - lang: PropTypes.string.isRequired, - name: PropTypes.string, - script: PropTypes.string, - executeScript: PropTypes.func.isRequired, -}; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js deleted file mode 100644 index 2fac8c7641ddb..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { ScriptingHelpFlyout } from './help_flyout'; - -jest.mock('ui/documentation_links', () => ({ - getDocLink: doc => `(docLink for ${doc})`, -})); - -jest.mock('./test_script', () => ({ - TestScript: () => { - return `
mockTestScript
`; - }, -})); - -const indexPatternMock = {}; - -describe('ScriptingHelpFlyout', () => { - it('should render normally', async () => { - const component = shallow( - {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if not visible', async () => { - const component = shallow( - {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx new file mode 100644 index 0000000000000..4106eb7b283ee --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { HttpStart } from 'src/core/public'; +// eslint-disable-next-line +import { docLinksServiceMock } from '../../../../../../core/public/doc_links/doc_links_service.mock'; + +import { ScriptingHelpFlyout } from './help_flyout'; + +import { IndexPattern } from '../../../../../../plugins/data/public'; + +import { ExecuteScript } from '../../types'; + +jest.mock('./test_script', () => ({ + TestScript: () => { + return `
mockTestScript
`; + }, +})); + +const indexPatternMock = {} as IndexPattern; + +describe('ScriptingHelpFlyout', () => { + const docLinksScriptedFields = docLinksServiceMock.createStartContract().links.scriptedFields; + it('should render normally', async () => { + const component = shallow( + {}) as unknown) as ExecuteScript} + onClose={() => {}} + getHttpStart={() => (({} as unknown) as HttpStart)} + // docLinksScriptedFields={docLinksScriptedFields} + docLinksScriptedFields={{} as typeof docLinksScriptedFields} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if not visible', async () => { + const component = shallow( + {}) as unknown) as ExecuteScript} + onClose={() => {}} + getHttpStart={() => (({} as unknown) as HttpStart)} + docLinksScriptedFields={{} as typeof docLinksScriptedFields} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx new file mode 100644 index 0000000000000..6f51379c796d4 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { HttpStart, DocLinksStart } from 'src/core/public'; + +import { EuiFlyout, EuiFlyoutBody, EuiTabbedContent } from '@elastic/eui'; + +import { ScriptingSyntax } from './scripting_syntax'; +import { TestScript } from './test_script'; + +import { IndexPattern } from '../../../../../../plugins/data/public'; +import { ExecuteScript } from '../../types'; + +interface ScriptingHelpFlyoutProps { + indexPattern: IndexPattern; + lang: string; + name?: string; + script?: string; + executeScript: ExecuteScript; + isVisible: boolean; + onClose: () => void; + getHttpStart: () => HttpStart; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingHelpFlyout: React.FC = ({ + isVisible = false, + onClose = () => {}, + indexPattern, + lang, + name, + script, + executeScript, + getHttpStart, + docLinksScriptedFields, +}) => { + const tabs = [ + { + id: 'syntax', + name: 'Syntax', + ['data-test-subj']: 'syntaxTab', + content: , + }, + { + id: 'test', + name: 'Preview results', + ['data-test-subj']: 'testTab', + content: ( + + ), + }, + ]; + + return isVisible ? ( + + + + + + ) : null; +}; + +ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/index.js b/src/legacy/ui/public/field_editor/components/scripting_help/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_help/index.js rename to src/legacy/ui/public/field_editor/components/scripting_help/index.ts diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js deleted file mode 100644 index ba47b94aaea0c..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Fragment } from 'react'; -import { getDocLink } from 'ui/documentation_links'; - -import { EuiCode, EuiIcon, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ScriptingSyntax = () => ( - - - -

- -

-

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

-

- - - -

-

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

-

- -

-

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

-

- -

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

- -

-
    -
  • - + - * / % }} - /> -
  • -
  • - | & ^ ~ << >> >>>, - }} - /> -
  • -
  • - && || ! ?: }} - /> -
  • -
  • - < <= == >= > }} - /> -
  • -
  • - abs ceil exp floor ln log10 logn max min sqrt pow }} - /> -
  • -
  • - acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan - ), - }} - /> -
  • -
  • - haversin }} - /> -
  • -
  • - min, max }} - /> -
  • -
-
-
-); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx new file mode 100644 index 0000000000000..8158c6881acf9 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx @@ -0,0 +1,218 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { EuiCode, EuiIcon, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ScriptingSyntaxProps { + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingSyntax = ({ docLinksScriptedFields }: ScriptingSyntaxProps) => ( + + + +

+ +

+

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

+

+ + + +

+

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

+

+ +

+

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

+

+ +

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

+ +

+
    +
  • + + - * / % }} + /> +
  • +
  • + | & ^ ~ << >> >>>, + }} + /> +
  • +
  • + && || ! ?: }} + /> +
  • +
  • + < <= == >= > }} + /> +
  • +
  • + abs ceil exp floor ln log10 logn max min sqrt pow }} + /> +
  • +
  • + acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan + ), + }} + /> +
  • +
  • + haversin }} + /> +
  • +
  • + min, max }} + /> +
  • +
+
+
+); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js deleted file mode 100644 index 12bf5c1cce004..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiButton, - EuiCodeBlock, - EuiComboBox, - EuiFormRow, - EuiText, - EuiSpacer, - EuiTitle, - EuiCallOut, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { npStart } from 'ui/new_platform'; -const { SearchBar } = npStart.plugins.data.ui; - -const { uiSettings } = npStart.core; - -import { esQuery } from '../../../../../../plugins/data/public'; - -export class TestScript extends Component { - state = { - isLoading: false, - additionalFields: [], - }; - - componentDidMount() { - if (this.props.script) { - this.previewScript(); - } - } - - previewScript = async searchContext => { - const { indexPattern, lang, name, script, executeScript } = this.props; - - if (!script || script.length === 0) { - return; - } - - this.setState({ - isLoading: true, - }); - - let query; - if (searchContext) { - const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - query = esQuery.buildEsQuery( - this.props.indexPattern, - searchContext.query, - null, - esQueryConfigs - ); - } - - const scriptResponse = await executeScript({ - name, - lang, - script, - indexPatternTitle: indexPattern.title, - query, - additionalFields: this.state.additionalFields.map(option => { - return option.value; - }), - }); - - if (scriptResponse.status !== 200) { - this.setState({ - isLoading: false, - previewData: scriptResponse, - }); - return; - } - - this.setState({ - isLoading: false, - previewData: scriptResponse.hits.hits.map(hit => ({ - _id: hit._id, - ...hit._source, - ...hit.fields, - })), - }); - }; - - onAdditionalFieldsChange = selectedOptions => { - this.setState({ - additionalFields: selectedOptions, - }); - }; - - renderPreview() { - const { previewData } = this.state; - - if (!previewData) { - return null; - } - - if (previewData.error) { - return ( - - - {JSON.stringify(previewData.error, null, ' ')} - - - ); - } - - return ( - - -

- -

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

- -

-

- -

-
- - {this.renderToolbar()} - - {this.renderPreview()} -
- ); - } -} - -TestScript.propTypes = { - indexPattern: PropTypes.object.isRequired, - lang: PropTypes.string.isRequired, - name: PropTypes.string, - script: PropTypes.string, - executeScript: PropTypes.func.isRequired, -}; - -TestScript.defaultProps = { - name: 'myScriptedField', -}; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx new file mode 100644 index 0000000000000..8d3a83a155553 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx @@ -0,0 +1,293 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component, Fragment } from 'react'; +import { HttpStart } from 'src/core/public'; + +import { + EuiButton, + EuiCodeBlock, + EuiComboBox, + EuiFormRow, + EuiText, + EuiSpacer, + EuiTitle, + EuiCallOut, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; + +const { uiSettings } = npStart.core; + +import { esQuery, IndexPattern, Query } from '../../../../../../plugins/data/public'; +import { ExecuteScript } from '../../types'; + +interface TestScriptProps { + indexPattern: IndexPattern; + lang: string; + name?: string; + script?: string; + executeScript: ExecuteScript; + getHttpStart: () => HttpStart; +} + +interface AdditionalField { + value: string; + label: string; +} + +interface TestScriptState { + isLoading: boolean; + additionalFields: AdditionalField[]; + previewData?: Record; +} + +export class TestScript extends Component { + defaultProps = { + name: 'myScriptedField', + }; + + state = { + isLoading: false, + additionalFields: [], + previewData: undefined, + }; + + componentDidMount() { + if (this.props.script) { + this.previewScript(); + } + } + + previewScript = async (searchContext?: { query?: Query | undefined }) => { + const { indexPattern, lang, name, script, executeScript, getHttpStart } = this.props; + + if (!script || script.length === 0) { + return; + } + + this.setState({ + isLoading: true, + }); + + let query; + if (searchContext) { + const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); + query = esQuery.buildEsQuery( + this.props.indexPattern, + searchContext.query || [], + [], + esQueryConfigs + ); + } + + const scriptResponse = await executeScript({ + name: name as string, + lang, + script, + indexPatternTitle: indexPattern.title, + query, + additionalFields: this.state.additionalFields.map((option: AdditionalField) => option.value), + getHttpStart, + }); + + if (scriptResponse.status !== 200) { + this.setState({ + isLoading: false, + previewData: scriptResponse, + }); + return; + } + + this.setState({ + isLoading: false, + previewData: scriptResponse.hits.hits.map(hit => ({ + _id: hit._id, + ...hit._source, + ...hit.fields, + })), + }); + }; + + onAdditionalFieldsChange = (selectedOptions: AdditionalField[]) => { + this.setState({ + additionalFields: selectedOptions, + }); + }; + + renderPreview(previewData: { error: any } | undefined) { + if (!previewData) { + return null; + } + + if (previewData.error) { + return ( + + + {JSON.stringify(previewData.error, null, ' ')} + + + ); + } + + return ( + + +

+ +

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

+ +

+

+ +

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

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

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

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

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

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

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

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

+
+ + + {this.renderScriptingPanels()} + {this.renderName()} + {this.renderLanguage()} + {this.renderType()} + {this.renderTypeConflict()} + {this.renderFormat()} + {this.renderPopularity()} + {this.renderScript()} + {this.renderActions()} + {this.renderDeleteModal()} + + +
+ ) : null; + } +} diff --git a/src/legacy/ui/public/field_editor/index.js b/src/legacy/ui/public/field_editor/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/index.js rename to src/legacy/ui/public/field_editor/index.ts diff --git a/src/legacy/ui/public/field_editor/lib/index.js b/src/legacy/ui/public/field_editor/lib/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/lib/index.js rename to src/legacy/ui/public/field_editor/lib/index.ts diff --git a/src/legacy/ui/public/field_editor/lib/validate_script.js b/src/legacy/ui/public/field_editor/lib/validate_script.js deleted file mode 100644 index 47e2091565c30..0000000000000 --- a/src/legacy/ui/public/field_editor/lib/validate_script.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { kfetch } from 'ui/kfetch'; - -export const executeScript = async ({ - name, - lang, - script, - indexPatternTitle, - query, - additionalFields = [], -}) => { - // Using _msearch because _search with index name in path dorks everything up - const header = { - index: indexPatternTitle, - ignore_unavailable: true, - }; - - const search = { - query: { - match_all: {}, - }, - script_fields: { - [name]: { - script: { - lang, - source: script, - }, - }, - }, - size: 10, - timeout: '30s', - }; - - if (additionalFields.length > 0) { - search._source = additionalFields; - } - - if (query) { - search.query = query; - } - - const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; - const esResp = await kfetch({ method: 'POST', pathname: '/elasticsearch/_msearch', body }); - // unwrap _msearch response - return esResp.responses[0]; -}; - -export const isScriptValid = async ({ name, lang, script, indexPatternTitle }) => { - const scriptResponse = await executeScript({ name, lang, script, indexPatternTitle }); - - if (scriptResponse.status !== 200) { - return false; - } - - return true; -}; diff --git a/src/legacy/ui/public/field_editor/lib/validate_script.ts b/src/legacy/ui/public/field_editor/lib/validate_script.ts new file mode 100644 index 0000000000000..4db827a4229fd --- /dev/null +++ b/src/legacy/ui/public/field_editor/lib/validate_script.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Query } from 'src/plugins/data/public'; +import { HttpStart } from 'src/core/public'; +import { ExecuteScriptParams, ExecuteScriptResult } from '../types'; + +export const executeScript = async ({ + name, + lang, + script, + indexPatternTitle, + query, + additionalFields = [], + getHttpStart, +}: ExecuteScriptParams): Promise => { + // Using _msearch because _search with index name in path dorks everything up + const header = { + index: indexPatternTitle, + ignore_unavailable: true, + }; + + const search = { + query: { + match_all: {}, + } as Query['query'], + script_fields: { + [name]: { + script: { + lang, + source: script, + }, + }, + }, + _source: undefined as string[] | undefined, + size: 10, + timeout: '30s', + }; + + if (additionalFields.length > 0) { + search._source = additionalFields; + } + + if (query) { + search.query = query; + } + + const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; + const http = await getHttpStart(); + const esResp = await http.fetch('/elasticsearch/_msearch', { method: 'POST', body }); + // unwrap _msearch response + return esResp.responses[0]; +}; + +export const isScriptValid = async ({ + name, + lang, + script, + indexPatternTitle, + getHttpStart, +}: { + name: string; + lang: string; + script: string; + indexPatternTitle: string; + getHttpStart: () => HttpStart; +}) => { + const scriptResponse = await executeScript({ + name, + lang, + script, + indexPatternTitle, + getHttpStart, + }); + + if (scriptResponse.status !== 200) { + return false; + } + + return true; +}; diff --git a/src/legacy/ui/public/field_editor/types.ts b/src/legacy/ui/public/field_editor/types.ts new file mode 100644 index 0000000000000..174cb7da73ceb --- /dev/null +++ b/src/legacy/ui/public/field_editor/types.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReactText } from 'react'; +import { Query } from 'src/plugins/data/public'; +import { HttpStart } from 'src/core/public'; + +export interface Sample { + input: ReactText | ReactText[]; + output: string; +} + +export interface ExecuteScriptParams { + name: string; + lang: string; + script: string; + indexPatternTitle: string; + query?: Query['query']; + additionalFields?: string[]; + getHttpStart: () => HttpStart; +} + +export interface ExecuteScriptResult { + status: number; + hits: { hits: any[] }; + error?: any; +} + +export type ExecuteScript = (params: ExecuteScriptParams) => Promise; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx index c7a778ac18bd3..be8ab4cf8d696 100644 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ b/src/legacy/ui/public/i18n/index.test.tsx @@ -21,6 +21,7 @@ import { render } from 'enzyme'; import PropTypes from 'prop-types'; import React from 'react'; +jest.mock('angular-sanitize', () => {}); jest.mock('ui/new_platform', () => ({ npStart: { core: { diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index c918554563fcb..6f1120dce0c7c 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -18,6 +18,8 @@ */ import React from 'react'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; // @ts-ignore diff --git a/src/legacy/ui/public/registry/field_format_editors.js b/src/legacy/ui/public/registry/field_format_editors.js deleted file mode 100644 index 85850e56fdb86..0000000000000 --- a/src/legacy/ui/public/registry/field_format_editors.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiRegistry } from './_registry'; - -export const RegistryFieldFormatEditorsProvider = uiRegistry({ - name: 'fieldFormatEditors', - index: ['formatId'], - constructor: function() { - this.getEditor = function(formatId) { - return this.byFormatId[formatId]; - }; - }, -}); diff --git a/src/legacy/ui/public/registry/field_format_editors.ts b/src/legacy/ui/public/registry/field_format_editors.ts new file mode 100644 index 0000000000000..5489ce9250e04 --- /dev/null +++ b/src/legacy/ui/public/registry/field_format_editors.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { uiRegistry } from './_registry'; + +export const RegistryFieldFormatEditorsProvider = uiRegistry({ + name: 'fieldFormatEditors', + index: ['formatId'], +}); diff --git a/src/legacy/ui/public/scripting_languages/index.ts b/src/legacy/ui/public/scripting_languages/index.ts index 283a3273a2a5d..459e72c0c67c1 100644 --- a/src/legacy/ui/public/scripting_languages/index.ts +++ b/src/legacy/ui/public/scripting_languages/index.ts @@ -17,10 +17,8 @@ * under the License. */ -import { IHttpService } from 'angular'; import { i18n } from '@kbn/i18n'; - -import chrome from '../chrome'; +import { HttpStart } from 'src/core/public'; import { toastNotifications } from '../notify'; export function getSupportedScriptingLanguages(): string[] { @@ -31,18 +29,12 @@ export function getDeprecatedScriptingLanguages(): string[] { return []; } -export function GetEnabledScriptingLanguagesProvider($http: IHttpService) { - return () => { - return $http - .get(chrome.addBasePath('/api/kibana/scripts/languages')) - .then((res: any) => res.data) - .catch(() => { - toastNotifications.addDanger( - i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { - defaultMessage: 'Error getting available scripting languages from Elasticsearch', - }) - ); - return []; - }); - }; -} +export const getEnabledScriptingLanguages = (http: HttpStart) => + http.get('/api/kibana/scripts/languages').catch(() => { + toastNotifications.addDanger( + i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { + defaultMessage: 'Error getting available scripting languages from Elasticsearch', + }) + ); + return []; + }); diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 8a71c6ccb1506..b60442690af3c 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 801eecf5b608b..9b44395fa9c68 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -19,13 +19,15 @@ import { createHash } from 'crypto'; import Boom from 'boom'; -import { resolve } from 'path'; +import Path from 'path'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; +const uniq = (...items) => Array.from(new Set(items)); + /** * @typedef {import('../../server/kbn_server').default} KbnServer * @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit @@ -39,7 +41,7 @@ import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; */ export function uiRenderMixin(kbnServer, server, config) { // render all views from ./views - server.setupViews(resolve(__dirname, 'views')); + server.setupViews(Path.resolve(__dirname, 'views')); const translationsCache = { translations: null, hash: null }; server.route({ @@ -94,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 0b2aeda11fb0e..902fa59b20569 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -32,6 +32,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder'; const chance = new Chance(); const outputFixture = resolve(__dirname, './fixtures/output'); +const pluginNoPlaceholderFixture = resolve(__dirname, './fixtures/plugin/no_placeholder'); const randomWordsCache = new Set(); const uniqueRandomWord = () => { @@ -58,6 +59,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath = outputFixture, basePublicPath = '', builtCssPath = outputFixture, + npUiPluginPublicDirs = [], + buildHash = '1234', + isDist = false, } = options; const server = new Hapi.Server(); @@ -69,6 +73,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs, + buildHash, + isDist, }) ); @@ -158,7 +165,7 @@ describe('optimizer/bundle route', () => { it('responds with exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/image.png', + url: '/1234/bundles/image.png', }); expect(response.statusCode).to.be(200); @@ -173,7 +180,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -187,12 +194,12 @@ describe('optimizer/bundle route', () => { }); describe('js file with placeholder', () => { - it('responds with no content-length and modified file data', async () => { + it('responds with no content-length and modifiedfile data ', async () => { const basePublicPath = `/${uniqueRandomWord()}`; const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -204,7 +211,7 @@ describe('optimizer/bundle route', () => { ); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -213,7 +220,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.css', + url: '/1234/bundles/no_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -231,7 +238,7 @@ describe('optimizer/bundle route', () => { const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.css', + url: '/1234/bundles/with_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -240,7 +247,7 @@ describe('optimizer/bundle route', () => { expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -250,7 +257,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/../outside_output.js', + url: '/1234/bundles/../outside_output.js', }); expect(response.statusCode).to.be(404); @@ -267,7 +274,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/non_existent.js', + url: '/1234/bundles/non_existent.js', }); expect(response.statusCode).to.be(404); @@ -286,7 +293,7 @@ describe('optimizer/bundle route', () => { }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(404); @@ -306,7 +313,7 @@ describe('optimizer/bundle route', () => { sinon.assert.notCalled(createHash); const resp1 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.calledOnce(createHash); @@ -314,23 +321,23 @@ describe('optimizer/bundle route', () => { expect(resp1.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.notCalled(createHash); expect(resp2.statusCode).to.be(200); }); - it('is unique per basePublicPath although content is the same', async () => { + it('is unique per basePublicPath although content is the same (by default)', async () => { const basePublicPath1 = `/${uniqueRandomWord()}`; const basePublicPath2 = `/${uniqueRandomWord()}`; const [resp1, resp2] = await Promise.all([ createServer({ basePublicPath: basePublicPath1 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), createServer({ basePublicPath: basePublicPath2 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), ]); @@ -349,13 +356,13 @@ describe('optimizer/bundle route', () => { it('responds with 304 when etag and last modified are sent back', async () => { const server = createServer(); const resp = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(resp.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', headers: { 'if-modified-since': resp.headers['last-modified'], 'if-none-match': resp.headers.etag, @@ -366,4 +373,80 @@ describe('optimizer/bundle route', () => { expect(resp2.result).to.have.length(0); }); }); + + describe('kibana platform assets', () => { + describe('caching', () => { + describe('for non-distributable mode', () => { + it('uses "etag" header to invalidate cache', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ basePublicPath, npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be.a('string'); + expect(responce.headers['cache-control']).to.be('must-revalidate'); + }); + + it('creates the same "etag" header for the same content with the same basePath', async () => { + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const [resp1, resp2] = await Promise.all([ + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + ]); + + expect(resp1.statusCode).to.be(200); + expect(resp2.statusCode).to.be(200); + + expect(resp1.rawPayload).to.eql(resp2.rawPayload); + + expect(resp1.headers.etag).to.be.a('string'); + expect(resp2.headers.etag).to.be.a('string'); + expect(resp1.headers.etag).to.eql(resp2.headers.etag); + }); + }); + + describe('for distributable mode', () => { + it('commands to cache assets for each release for a year', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ + basePublicPath, + npUiPluginPublicDirs, + isDist: true, + }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be(undefined); + expect(responce.headers['cache-control']).to.be('max-age=31536000'); + }); + }); + }); + }); }); diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js deleted file mode 100644 index 4030988c8552c..0000000000000 --- 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 0000000000000..e9cfba0130d95 --- /dev/null +++ b/src/optimize/bundles_route/bundles_route.ts @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isAbsolute, extname, join } from 'path'; + +import Hapi from 'hapi'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; + +import { createDynamicAssetResponse } from './dynamic_asset_response'; +import { FileHashCache } from './file_hash_cache'; +import { assertIsNpUiPluginPublicDirs, NpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; +import { fromRoot } from '../../core/server/utils'; + +/** + * Creates the routes that serves files from `bundlesPath` or from + * `dllBundlesPath` (if they are dll bundle's related files). If the + * file is js or css then it is searched for instances of + * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. + * + * @param {Object} options + * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins + * @property {string} options.regularBundlesPath + * @property {string} options.dllBundlesPath + * @property {string} options.basePublicPath + * + * @return Array.of({Hapi.Route}) + */ +export function createBundlesRoute({ + regularBundlesPath, + dllBundlesPath, + basePublicPath, + builtCssPath, + npUiPluginPublicDirs = [], + buildHash, + isDist = false, +}: { + regularBundlesPath: string; + dllBundlesPath: string; + basePublicPath: string; + builtCssPath: string; + npUiPluginPublicDirs?: NpUiPluginPublicDirs; + buildHash: string; + isDist?: boolean; +}) { + // rather than calculate the fileHash on every request, we + // provide a cache object to `resolveDynamicAssetResponse()` that + // will store the 100 most recently used hashes. + const fileHashCache = new FileHashCache(); + assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); + + if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { + throw new TypeError( + 'regularBundlesPath must be an absolute path to the directory containing the regular bundles' + ); + } + + if (typeof dllBundlesPath !== 'string' || !isAbsolute(dllBundlesPath)) { + throw new TypeError( + 'dllBundlesPath must be an absolute path to the directory containing the dll bundles' + ); + } + + if (typeof basePublicPath !== 'string') { + throw new TypeError('basePublicPath must be a string'); + } + + if (!basePublicPath.match(/(^$|^\/.*[^\/]$)/)) { + throw new TypeError('basePublicPath must be empty OR start and not end with a /'); + } + + return [ + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`, + routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`, + bundlesPath: UiSharedDeps.distDir, + fileHashCache, + replacePublicPath: false, + isDist, + }), + ...npUiPluginPublicDirs.map(({ id, path }) => + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`, + routePath: `/${buildHash}/bundles/plugin/${id}/`, + bundlesPath: path, + fileHashCache, + replacePublicPath: false, + isDist, + }) + ), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/core/`, + routePath: `/${buildHash}/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + replacePublicPath: false, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/`, + routePath: `/${buildHash}/bundles/`, + bundlesPath: regularBundlesPath, + fileHashCache, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`, + routePath: `/${buildHash}/built_assets/dlls/`, + bundlesPath: dllBundlesPath, + fileHashCache, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/`, + routePath: `/${buildHash}/built_assets/css/`, + bundlesPath: builtCssPath, + fileHashCache, + isDist, + }), + ]; +} + +function buildRouteForBundles({ + publicPath, + routePath, + bundlesPath, + fileHashCache, + replacePublicPath = true, + isDist, +}: { + publicPath: string; + routePath: string; + bundlesPath: string; + fileHashCache: FileHashCache; + replacePublicPath?: boolean; + isDist: boolean; +}) { + return { + method: 'GET', + path: `${routePath}{path*}`, + config: { + auth: false, + ext: { + onPreHandler: { + method(request: Hapi.Request, h: Hapi.ResponseToolkit) { + const ext = extname(request.params.path); + + if (ext !== '.js' && ext !== '.css') { + return h.continue; + } + + return createDynamicAssetResponse({ + request, + h, + bundlesPath, + fileHashCache, + publicPath, + replacePublicPath, + isDist, + }); + }, + }, + }, + }, + handler: { + directory: { + path: bundlesPath, + listing: false, + lookupCompressed: true, + }, + }, + }; +} diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js deleted file mode 100644 index 80c49a26270fd..0000000000000 --- 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 0000000000000..a020c6935eeec --- /dev/null +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import { resolve } from 'path'; +import { promisify } from 'util'; + +import Boom from 'boom'; +import Hapi from 'hapi'; + +import { FileHashCache } from './file_hash_cache'; +import { getFileHash } from './file_hash'; +// @ts-ignore +import { replacePlaceholder } from '../public_path_placeholder'; + +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +const asyncOpen = promisify(Fs.open); +const asyncClose = promisify(Fs.close); +const asyncFstat = promisify(Fs.fstat); + +/** + * Create a Hapi response for the requested path. This is designed + * to replicate a subset of the features provided by Hapi's Inert + * plugin including: + * - ensure path is not traversing out of the bundle directory + * - manage use file descriptors for file access to efficiently + * interact with the file multiple times in each request + * - generate and cache etag for the file + * - write correct headers to response for client-side caching + * and invalidation + * - stream file to response + * + * It differs from Inert in some important ways: + * - the PUBLIC_PATH_PLACEHOLDER is replaced with the correct + * public path as the response is streamed + * - cached hash/etag is based on the file on disk, but modified + * by the public path so that individual public paths have + * different etags, but can share a cache + */ +export async function createDynamicAssetResponse({ + request, + h, + bundlesPath, + publicPath, + fileHashCache, + replacePublicPath, + isDist, +}: { + request: Hapi.Request; + h: Hapi.ResponseToolkit; + bundlesPath: string; + publicPath: string; + fileHashCache: FileHashCache; + replacePublicPath: boolean; + isDist: boolean; +}) { + let fd: number | undefined; + + try { + const path = resolve(bundlesPath, request.params.path); + + // prevent path traversal, only process paths that resolve within bundlesPath + if (!path.startsWith(bundlesPath)) { + throw Boom.forbidden(undefined, 'EACCES'); + } + + // we use and manage a file descriptor mostly because + // that's what Inert does, and since we are accessing + // the file 2 or 3 times per request it seems logical + fd = await asyncOpen(path, 'r'); + + const stat = await asyncFstat(fd); + const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); + + const read = Fs.createReadStream(null as any, { + fd, + start: 0, + autoClose: true, + }); + fd = undefined; // read stream is now responsible for fd + + const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; + + const response = h + .response(content) + .takeover() + .code(200) + .type(request.server.mime.path(path).type); + + if (isDist) { + response.header('cache-control', `max-age=${365 * DAY}`); + } else { + response.etag(`${hash}-${publicPath}`); + response.header('cache-control', 'must-revalidate'); + } + + return response; + } catch (error) { + if (fd) { + try { + await asyncClose(fd); + } catch (_) { + // ignore errors from close, we already have one to report + // and it's very likely they are the same + } + } + + if (error.code === 'ENOENT') { + throw Boom.notFound(); + } + + throw Boom.boomify(error); + } +} diff --git a/src/optimize/bundles_route/file_hash.js b/src/optimize/bundles_route/file_hash.js deleted file mode 100644 index d9464cf05eca1..0000000000000 --- a/src/optimize/bundles_route/file_hash.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createHash } from 'crypto'; -import { createReadStream } from 'fs'; - -import * as Rx from 'rxjs'; -import { merge, mergeMap, takeUntil } from 'rxjs/operators'; - -/** - * Get the hash of a file via a file descriptor - * @param {LruCache} cache - * @param {string} path - * @param {Fs.Stat} stat - * @param {Fs.FileDescriptor} fd - * @return {Promise} - */ -export async function getFileHash(cache, path, stat, fd) { - const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; - - const cached = cache.get(key); - if (cached) { - return await cached; - } - - const hash = createHash('sha1'); - const read = createReadStream(null, { - fd, - start: 0, - autoClose: false, - }); - - const promise = Rx.fromEvent(read, 'data') - .pipe( - merge(Rx.fromEvent(read, 'error').pipe(mergeMap(Rx.throwError))), - takeUntil(Rx.fromEvent(read, 'end')) - ) - .forEach(chunk => hash.update(chunk)) - .then(() => hash.digest('hex')) - .catch(error => { - // don't cache failed attempts - cache.del(key); - throw error; - }); - - cache.set(key, promise); - return await promise; -} diff --git a/src/optimize/bundles_route/file_hash.ts b/src/optimize/bundles_route/file_hash.ts new file mode 100644 index 0000000000000..7b0801098ed10 --- /dev/null +++ b/src/optimize/bundles_route/file_hash.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHash } from 'crypto'; +import Fs from 'fs'; + +import * as Rx from 'rxjs'; +import { takeUntil, map } from 'rxjs/operators'; + +import { FileHashCache } from './file_hash_cache'; + +/** + * Get the hash of a file via a file descriptor + */ +export async function getFileHash(cache: FileHashCache, path: string, stat: Fs.Stats, fd: number) { + const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; + + const cached = cache.get(key); + if (cached) { + return await cached; + } + + const hash = createHash('sha1'); + const read = Fs.createReadStream(null as any, { + fd, + start: 0, + autoClose: false, + }); + + const promise = Rx.merge( + Rx.fromEvent(read, 'data'), + Rx.fromEvent(read, 'error').pipe( + map(error => { + throw error; + }) + ) + ) + .pipe(takeUntil(Rx.fromEvent(read, 'end'))) + .forEach(chunk => hash.update(chunk)) + .then(() => hash.digest('hex')) + .catch(error => { + // don't cache failed attempts + cache.del(key); + throw error; + }); + + cache.set(key, promise); + return await promise; +} diff --git a/src/optimize/bundles_route/file_hash_cache.ts b/src/optimize/bundles_route/file_hash_cache.ts new file mode 100644 index 0000000000000..a7cdabbff13a7 --- /dev/null +++ b/src/optimize/bundles_route/file_hash_cache.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import LruCache from 'lru-cache'; + +export class FileHashCache { + private lru = new LruCache>(100); + + get(key: string) { + return this.lru.get(key); + } + + set(key: string, value: Promise) { + this.lru.set(key, value); + } + + del(key: string) { + this.lru.del(key); + } +} diff --git a/src/optimize/bundles_route/index.js b/src/optimize/bundles_route/index.ts similarity index 100% rename from src/optimize/bundles_route/index.js rename to src/optimize/bundles_route/index.ts diff --git a/src/optimize/bundles_route/proxy_bundles_route.js b/src/optimize/bundles_route/proxy_bundles_route.js deleted file mode 100644 index fff0ec444d95b..0000000000000 --- a/src/optimize/bundles_route/proxy_bundles_route.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function createProxyBundlesRoute({ host, port }) { - return [ - buildProxyRouteForBundles('/bundles/', host, port), - buildProxyRouteForBundles('/built_assets/dlls/', host, port), - buildProxyRouteForBundles('/built_assets/css/', host, port), - ]; -} - -function buildProxyRouteForBundles(routePath, host, port) { - return { - path: `${routePath}{path*}`, - method: 'GET', - handler: { - proxy: { - host, - port, - passThrough: true, - xforward: true, - }, - }, - config: { auth: false }, - }; -} diff --git a/src/optimize/bundles_route/proxy_bundles_route.ts b/src/optimize/bundles_route/proxy_bundles_route.ts new file mode 100644 index 0000000000000..1d189054324a1 --- /dev/null +++ b/src/optimize/bundles_route/proxy_bundles_route.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function createProxyBundlesRoute({ + host, + port, + buildHash, +}: { + host: string; + port: number; + buildHash: string; +}) { + return [ + buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port), + ]; +} + +function buildProxyRouteForBundles(routePath: string, host: string, port: number) { + return { + path: `${routePath}{path*}`, + method: 'GET', + handler: { + proxy: { + host, + port, + passThrough: true, + xforward: true, + }, + }, + config: { auth: false }, + }; +} diff --git a/src/optimize/index.js b/src/optimize/index.js index b7b9f7712358a..363f81a6a3a96 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -17,72 +17,5 @@ * under the License. */ -import FsOptimizer from './fs_optimizer'; -import { createBundlesRoute } from './bundles_route'; -import { DllCompiler } from './dynamic_dll_plugin'; -import { fromRoot } from '../core/server/utils'; -import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; - -export default async (kbnServer, server, config) => { - if (!config.get('optimize.enabled')) return; - - // the watch optimizer sets up two threads, one is the server listening - // on 5601 and the other is a server listening on 5602 that builds the - // bundles in a "middleware" style. - // - // the server listening on 5601 may be restarted a number of times, depending - // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* - // requests to the other server. The server on 5602 is long running, in order - // to prevent complete rebuilds of the optimize content. - const watch = config.get('optimize.watch'); - if (watch) { - return await kbnServer.mixin(require('./watch/watch')); - } - - const { uiBundles } = kbnServer; - server.route( - createBundlesRoute({ - regularBundlesPath: uiBundles.getWorkingDir(), - dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, - basePublicPath: config.get('server.basePath'), - builtCssPath: fromRoot('built_assets/css'), - npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), - }) - ); - - // in prod, only bundle when something is missing or invalid - const reuseCache = config.get('optimize.useBundleCache') - ? await uiBundles.areAllBundleCachesValid() - : false; - - // we might not have any work to do - if (reuseCache) { - server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); - return; - } - - await uiBundles.resetBundleDir(); - - // only require the FsOptimizer when we need to - const optimizer = new FsOptimizer({ - logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), - uiBundles, - profile: config.get('optimize.profile'), - sourceMaps: config.get('optimize.sourceMaps'), - workers: config.get('optimize.workers'), - }); - - server.log( - ['info', 'optimize'], - `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` - ); - - const start = Date.now(); - await optimizer.run(); - const seconds = ((Date.now() - start) / 1000).toFixed(2); - - server.log( - ['info', 'optimize'], - `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` - ); -}; +import { optimizeMixin } from './optimize_mixin'; +export default optimizeMixin; diff --git a/src/optimize/np_ui_plugin_public_dirs.js b/src/optimize/np_ui_plugin_public_dirs.js deleted file mode 100644 index de05fd2b863b8..0000000000000 --- a/src/optimize/np_ui_plugin_public_dirs.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function getNpUiPluginPublicDirs(kbnServer) { - return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( - ([id, { publicTargetDir }]) => ({ - id, - path: publicTargetDir, - }) - ); -} - -export function isNpUiPluginPublicDirs(something) { - return ( - Array.isArray(something) && - something.every( - s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' - ) - ); -} - -export function assertIsNpUiPluginPublicDirs(something) { - if (!isNpUiPluginPublicDirs(something)) { - throw new TypeError( - 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' - ); - } -} diff --git a/src/optimize/np_ui_plugin_public_dirs.ts b/src/optimize/np_ui_plugin_public_dirs.ts new file mode 100644 index 0000000000000..e7c3207948f6a --- /dev/null +++ b/src/optimize/np_ui_plugin_public_dirs.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import KbnServer from '../legacy/server/kbn_server'; + +export type NpUiPluginPublicDirs = Array<{ + id: string; + path: string; +}>; + +export function getNpUiPluginPublicDirs(kbnServer: KbnServer): NpUiPluginPublicDirs { + return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( + ([id, { publicTargetDir }]) => ({ + id, + path: publicTargetDir, + }) + ); +} + +export function isNpUiPluginPublicDirs(x: any): x is NpUiPluginPublicDirs { + return ( + Array.isArray(x) && + x.every( + s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' + ) + ); +} + +export function assertIsNpUiPluginPublicDirs(x: any): asserts x is NpUiPluginPublicDirs { + if (!isNpUiPluginPublicDirs(x)) { + throw new TypeError( + 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' + ); + } +} diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts new file mode 100644 index 0000000000000..9a3f08e2f667e --- /dev/null +++ b/src/optimize/optimize_mixin.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Hapi from 'hapi'; + +// @ts-ignore not TS yet +import FsOptimizer from './fs_optimizer'; +import { createBundlesRoute } from './bundles_route'; +// @ts-ignore not TS yet +import { DllCompiler } from './dynamic_dll_plugin'; +import { fromRoot } from '../core/server/utils'; +import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; +import KbnServer, { KibanaConfig } from '../legacy/server/kbn_server'; + +export const optimizeMixin = async ( + kbnServer: KbnServer, + server: Hapi.Server, + config: KibanaConfig +) => { + if (!config.get('optimize.enabled')) return; + + // the watch optimizer sets up two threads, one is the server listening + // on 5601 and the other is a server listening on 5602 that builds the + // bundles in a "middleware" style. + // + // the server listening on 5601 may be restarted a number of times, depending + // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* + // requests to the other server. The server on 5602 is long running, in order + // to prevent complete rebuilds of the optimize content. + const watch = config.get('optimize.watch'); + if (watch) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return await kbnServer.mixin(require('./watch/watch')); + } + + const { uiBundles } = kbnServer; + server.route( + createBundlesRoute({ + regularBundlesPath: uiBundles.getWorkingDir(), + dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, + basePublicPath: config.get('server.basePath'), + builtCssPath: fromRoot('built_assets/css'), + npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), + isDist: kbnServer.newPlatform.env.packageInfo.dist, + }) + ); + + // in prod, only bundle when something is missing or invalid + const reuseCache = config.get('optimize.useBundleCache') + ? await uiBundles.areAllBundleCachesValid() + : false; + + // we might not have any work to do + if (reuseCache) { + server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); + return; + } + + await uiBundles.resetBundleDir(); + + // only require the FsOptimizer when we need to + const optimizer = new FsOptimizer({ + logWithMetadata: server.logWithMetadata, + uiBundles, + profile: config.get('optimize.profile'), + sourceMaps: config.get('optimize.sourceMaps'), + workers: config.get('optimize.workers'), + }); + + server.log( + ['info', 'optimize'], + `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` + ); + + const start = Date.now(); + await optimizer.run(); + const seconds = ((Date.now() - start) / 1000).toFixed(2); + + server.log( + ['info', 'optimize'], + `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` + ); +}; diff --git a/src/optimize/public_path_placeholder.js b/src/optimize/public_path_placeholder.js deleted file mode 100644 index ef05d9e5ae704..0000000000000 --- a/src/optimize/public_path_placeholder.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createReplaceStream } from '../legacy/utils'; - -import * as Rx from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; - -export const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; - -export function replacePlaceholder(read, replacement) { - const replace = createReplaceStream(PUBLIC_PATH_PLACEHOLDER, replacement); - - // handle errors on the read stream by proxying them - // to the replace stream so that the consumer can - // choose what to do with them. - Rx.fromEvent(read, 'error') - .pipe(take(1), takeUntil(Rx.fromEvent(read, 'end'))) - .forEach(error => { - replace.emit('error', error); - replace.end(); - }); - - replace.close = () => { - read.unpipe(); - - if (read.close) { - read.close(); - } - }; - - return read.pipe(replace); -} diff --git a/src/optimize/public_path_placeholder.ts b/src/optimize/public_path_placeholder.ts new file mode 100644 index 0000000000000..1ec2b4a431aa6 --- /dev/null +++ b/src/optimize/public_path_placeholder.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Stream from 'stream'; +import Fs from 'fs'; + +import * as Rx from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { createReplaceStream } from '../legacy/utils'; + +export const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; + +interface ClosableTransform extends Stream.Transform { + close(): void; +} + +export function replacePlaceholder(read: Stream.Readable, replacement: string) { + const replace = createReplaceStream(PUBLIC_PATH_PLACEHOLDER, replacement); + + // handle errors on the read stream by proxying them + // to the replace stream so that the consumer can + // choose what to do with them. + Rx.fromEvent(read, 'error') + .pipe(take(1), takeUntil(Rx.fromEvent(read, 'end'))) + .forEach(error => { + replace.emit('error', error); + replace.end(); + }); + + const closableReplace: ClosableTransform = Object.assign(replace, { + close: () => { + read.unpipe(); + + if ('close' in read) { + (read as Fs.ReadStream).close(); + } + }, + }); + + return read.pipe(closableReplace); +} diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index a31ef7229e5da..1f6107996277c 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -49,7 +49,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchPort'), config.get('server.basePath'), watchOptimizer, - getNpUiPluginPublicDirs(kbnServer) + getNpUiPluginPublicDirs(kbnServer), + kbnServer.newPlatform.env.packageInfo.buildNum.toString() ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/proxy_role.js b/src/optimize/watch/proxy_role.js index 6093658ae1a2d..0f6f3b2d4b622 100644 --- a/src/optimize/watch/proxy_role.js +++ b/src/optimize/watch/proxy_role.js @@ -26,6 +26,7 @@ export default (kbnServer, server, config) => { createProxyBundlesRoute({ host: config.get('optimize.watchHost'), port: config.get('optimize.watchPort'), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), }) ); diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 6c20f21c7768e..cdff57a00c2e0 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath, npUiPluginPublicDirs) { + bindToServer(server, basePath, npUiPluginPublicDirs, buildHash) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -118,6 +118,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ npUiPluginPublicDirs: npUiPluginPublicDirs, + buildHash, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index 74a96dc8aea6e..81e04a5b83956 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,10 +21,11 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs, buildHash) { this.basePath = basePath; this.optimizer = optimizer; this.npUiPluginPublicDirs = npUiPluginPublicDirs; + this.buildHash = buildHash; this.server = new Server({ host: host, port: port, @@ -35,7 +36,12 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); + this.optimizer.bindToServer( + this.server, + this.basePath, + this.npUiPluginPublicDirs, + this.buildHash + ); await this.server.start(); } } diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index a1696298117b0..37f014d836075 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -21,6 +21,8 @@ import './index.scss'; import { EuiIcon } from '@elastic/eui'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext, diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 96d0024dff2a2..26f07a12067ce 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -127,12 +127,12 @@ export abstract class FieldFormat { */ getConverterFor( contentType: FieldFormatsContentType = DEFAULT_CONTEXT_TYPE - ): FieldFormatConvertFunction | null { + ): FieldFormatConvertFunction { if (!this.convertObject) { this.convertObject = this.setupContentType(); } - return this.convertObject[contentType] || null; + return this.convertObject[contentType]; } /** diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index b0a57ad6912a7..2eb9a3e593d1a 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -110,7 +110,7 @@ export class FieldFormatsRegistry { */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, - esTypes: ES_FIELD_TYPES[] + esTypes?: ES_FIELD_TYPES[] ): FieldFormatInstanceType | undefined => { const config = this.getDefaultConfig(fieldType, esTypes); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 698edbf9cd6a8..e21d27a70e02a 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -26,6 +26,7 @@ export interface IIndexPattern { id?: string; type?: string; timeFieldName?: string; + getTimeField?(): IFieldType | undefined; fieldFormatMap?: Record< string, { diff --git a/src/plugins/data/common/utils/abort_utils.ts b/src/plugins/data/common/utils/abort_utils.ts index 5051515f3a826..9aec787170840 100644 --- a/src/plugins/data/common/utils/abort_utils.ts +++ b/src/plugins/data/common/utils/abort_utils.ts @@ -33,19 +33,31 @@ export class AbortError extends Error { * Returns a `Promise` corresponding with when the given `AbortSignal` is aborted. Useful for * situations when you might need to `Promise.race` multiple `AbortSignal`s, or an `AbortSignal` * with any other expected errors (or completions). + * * @param signal The `AbortSignal` to generate the `Promise` from * @param shouldReject If `false`, the promise will be resolved, otherwise it will be rejected */ -export function toPromise(signal: AbortSignal, shouldReject = false) { - return new Promise((resolve, reject) => { +export function toPromise(signal: AbortSignal, shouldReject?: false): Promise; +export function toPromise(signal: AbortSignal, shouldReject?: true): Promise; +export function toPromise(signal: AbortSignal, shouldReject: boolean = false) { + const promise = new Promise((resolve, reject) => { const action = shouldReject ? reject : resolve; if (signal.aborted) action(); signal.addEventListener('abort', action); }); + + /** + * Below is to make sure we don't have unhandled promise rejections. Otherwise + * Jest tests fail. + */ + promise.catch(() => {}); + + return promise; } /** * Returns an `AbortSignal` that will be aborted when the first of the given signals aborts. + * * @param signals */ export function getCombinedSignal(signals: AbortSignal[]) { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e1e2576b2a0e7..60d8079b22347 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 0fb92393d56f7..d83c0a7d3445e 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -47,6 +47,7 @@ export class Field implements IFieldType { indexPattern?: IndexPattern; format: any; $$spec: FieldSpec; + conflictDescriptions?: Record; constructor( indexPattern: IndexPattern, diff --git a/src/plugins/data/public/index_patterns/fields/field_list.ts b/src/plugins/data/public/index_patterns/fields/field_list.ts index d6067280fd7b6..9772370199b24 100644 --- a/src/plugins/data/public/index_patterns/fields/field_list.ts +++ b/src/plugins/data/public/index_patterns/fields/field_list.ts @@ -29,6 +29,7 @@ export interface IFieldList extends Array { getByType(type: Field['type']): Field[]; add(field: FieldSpec): void; remove(field: IFieldType): void; + update(field: FieldSpec): void; } export class FieldList extends Array implements IFieldList { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f5177df022ff2..91dea66f06a94 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) @@ -601,7 +699,10 @@ export function getSearchErrorType({ message }: Pick): " // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; // Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -744,6 +845,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; @@ -825,17 +928,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 +1015,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 +1848,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 +1863,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/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a8eb3a3fe8102..4dba157a6f554 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -51,5 +51,43 @@ describe('get_time', () => { }); clock.restore(); }); + + test('build range filter for non-primary field', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); + + const filter = getTime( + { + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as any, + { from: 'now-60y', to: 'now' }, + { fieldName: 'myCustomDate' } + ); + expect(filter!.range.myCustomDate).toEqual({ + gte: '1940-02-01T00:00:00.000Z', + lte: '2000-02-01T00:00:00.000Z', + format: 'strict_date_optional_time', + }); + clock.restore(); + }); }); }); diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index fa15406189041..9cdd25d3213ce 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { IIndexPattern } from '../..'; -import { TimeRange, IFieldType, buildRangeFilter } from '../../../common'; +import { TimeRange, buildRangeFilter } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; @@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, + options?: { forceNow?: Date; fieldName?: string } +) { + return createTimeRangeFilter( + indexPattern, + timeRange, + options?.fieldName || indexPattern?.timeFieldName, + options?.forceNow + ); +} + +function createTimeRangeFilter( + indexPattern: IIndexPattern | undefined, + timeRange: TimeRange, + fieldName?: string, forceNow?: Date ) { if (!indexPattern) { - // in CI, we sometimes seem to fail here. return; } - - const timefield: IFieldType | undefined = indexPattern.fields.find( - field => field.name === indexPattern.timeFieldName - ); - - if (!timefield) { + const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName)); + if (!field) { return; } @@ -55,7 +64,7 @@ export function getTime( return; } return buildRangeFilter( - timefield, + field, { ...(bounds.min && { gte: bounds.min.toISOString() }), ...(bounds.max && { lte: bounds.max.toISOString() }), diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12f..034af03842ab8 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; -export { getTime } from './get_time'; +export { getTime, calculateBounds } from './get_time'; export { changeTimeFilter } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 4fbdac47fb3b0..86ef69be572a9 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -164,7 +164,9 @@ export class Timefilter { }; public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => { - return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); + return getTime(indexPattern, timeRange ? timeRange : this._time, { + forceNow: this.getForceNow(), + }); }; public getBounds(): TimeRangeBounds { diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 57f3aa85ad944..3ecdc17cb57f3 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -45,7 +45,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && agg.fieldIsTimeField() + agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') ? timefilter.calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 087b83127079f..eec75b0841133 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; -import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; -import { FilterManager, getTime } from '../../query'; +import { + Filter, + Query, + serializeFieldFormat, + TimeRange, + IIndexPattern, + isRangeFilter, +} from '../../../common'; +import { FilterManager, calculateBounds, getTime } from '../../query'; import { getSearchService, getQueryService, getIndexPatterns } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils'; @@ -42,6 +49,8 @@ export interface RequestHandlerParams { searchSource: ISearchSource; aggs: IAggConfigs; timeRange?: TimeRange; + timeFields?: string[]; + indexPattern?: IIndexPattern; query?: Query; filters?: Filter[]; forceFetch: boolean; @@ -65,12 +74,15 @@ interface Arguments { partialRows: boolean; includeFormatHints: boolean; aggConfigs: string; + timeFields?: string[]; } const handleCourierRequest = async ({ searchSource, aggs, timeRange, + timeFields, + indexPattern, query, filters, forceFetch, @@ -111,9 +123,19 @@ const handleCourierRequest = async ({ return aggs.onSearchRequestStart(paramSearchSource, options); }); - if (timeRange) { + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { - return getTime(searchSource.getField('index'), timeRange); + return allTimeFields + .map(fieldName => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); }); } @@ -181,11 +203,13 @@ const handleCourierRequest = async ({ (searchSource as any).finalResponse = resp; - const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; const tabifyParams = { metricsAtAllLevels, partialRows, - timeRange: parsedTimeRange ? parsedTimeRange.range : undefined, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, }; const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); @@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition { const check = (aggResp: any, count: number, keys: string[]) => { @@ -187,9 +192,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -204,9 +209,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -221,9 +226,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 100, - lte: 400, - name: 'date', + from: moment(100), + to: moment(400), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -238,13 +243,47 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); expect(buckets).toHaveLength(4); }); + + test('does drop bucket when multiple time fields specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: ['date', 'other_datefield'], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]); + }); + + test('does not drop bucket when no timeFields have been specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: [], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]); + }); }); }); diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts index 971e820ac6ddf..cd52a09caeaad 100644 --- a/src/plugins/data/public/search/tabify/buckets.ts +++ b/src/plugins/data/public/search/tabify/buckets.ts @@ -20,7 +20,7 @@ import { get, isPlainObject, keys, findKey } from 'lodash'; import moment from 'moment'; import { IAggConfig } from '../aggs'; -import { AggResponseBucket, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types'; type AggParams = IAggConfig['params'] & { drop_partials: boolean; @@ -36,7 +36,7 @@ export class TabifyBuckets { buckets: any; _keys: any[] = []; - constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) { if (aggResp && aggResp.buckets) { this.buckets = aggResp.buckets; } else if (aggResp) { @@ -107,12 +107,12 @@ export class TabifyBuckets { // dropPartials should only be called if the aggParam setting is enabled, // and the agg field is the same as the Time Range. - private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) { if ( !timeRange || this.buckets.length <= 1 || this.objectMode || - params.field.name !== timeRange.name + !timeRange.timeFields.includes(params.field.name) ) { return; } @@ -120,10 +120,10 @@ export class TabifyBuckets { const interval = this.buckets[1].key - this.buckets[0].key; this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { - if (moment(bucket.key).isBefore(timeRange.gte)) { + if (moment(bucket.key).isBefore(timeRange.from)) { return false; } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + if (moment(bucket.key + interval).isAfter(timeRange.to)) { return false; } return true; diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts index e93e989034252..9cb55f94537c5 100644 --- a/src/plugins/data/public/search/tabify/tabify.ts +++ b/src/plugins/data/public/search/tabify/tabify.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { TabbedAggResponseWriter } from './response_writer'; import { TabifyBuckets } from './buckets'; -import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { TabbedResponseWriterOptions } from './types'; import { AggResponseBucket } from './types'; import { AggGroupNames, IAggConfigs } from '../aggs'; @@ -54,7 +54,7 @@ export function tabifyAggResponse( switch (agg.type.type) { case AggGroupNames.Buckets: const aggBucket = get(bucket, agg.id); - const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange); if (tabifyBuckets.length) { tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { @@ -153,20 +153,6 @@ export function tabifyAggResponse( doc_count: esResponse.hits.total, }; - let timeRange: TabbedRangeFilterParams | undefined; - - // Extract the time range object if provided - if (respOpts && respOpts.timeRange) { - const [timeRangeKey] = Object.keys(respOpts.timeRange); - - if (timeRangeKey) { - timeRange = { - name: timeRangeKey, - ...respOpts.timeRange[timeRangeKey], - }; - } - } - collectBucket(aggConfigs, write, topLevelBucket, '', 1); return write.response(); diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts index 1e051880d3f19..72e91eb58c8a9 100644 --- a/src/plugins/data/public/search/tabify/types.ts +++ b/src/plugins/data/public/search/tabify/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Moment } from 'moment'; import { RangeFilterParams } from '../../../common'; import { IAggConfig } from '../aggs'; @@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams { name: string; } +/** @internal */ +export interface TimeRangeInformation { + from?: Moment; + to?: Moment; + timeFields: string[]; +} + /** @internal **/ export interface TabbedResponseWriterOptions { metricsAtAllLevels: boolean; partialRows: boolean; - timeRange?: { [key: string]: RangeFilterParams }; + timeRange?: TimeRangeInformation; } /** @internal */ diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 9b673de60ca65..df4ba23244b4d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -408,6 +408,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; @@ -609,7 +611,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/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts new file mode 100644 index 0000000000000..ecbf94eceae64 --- /dev/null +++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Execution } from './execution'; +import { parseExpression } from '../ast'; +import { createUnitTestExecutor } from '../test_helpers'; + +jest.useFakeTimers(); + +beforeEach(() => { + jest.clearAllTimers(); +}); + +const createExecution = ( + expression: string = 'foo bar=123', + context: Record = {}, + debug: boolean = false +) => { + const executor = createUnitTestExecutor(); + const execution = new Execution({ + executor, + ast: parseExpression(expression), + context, + debug, + }); + return execution; +}; + +describe('Execution abortion tests', () => { + test('can abort an expression immediately', async () => { + const execution = createExecution('sleep 10'); + + execution.start(); + execution.cancel(); + + const result = await execution.result; + + expect(result).toMatchObject({ + type: 'error', + error: { + message: 'The expression was aborted.', + name: 'AbortError', + }, + }); + }); + + test('can abort an expression which has function running mid flight', async () => { + const execution = createExecution('sleep 300'); + + execution.start(); + jest.advanceTimersByTime(100); + execution.cancel(); + + const result = await execution.result; + + expect(result).toMatchObject({ + type: 'error', + error: { + message: 'The expression was aborted.', + name: 'AbortError', + }, + }); + }); + + test('cancelling execution after it completed has no effect', async () => { + jest.useRealTimers(); + + const execution = createExecution('sleep 1'); + + execution.start(); + + const result = await execution.result; + + execution.cancel(); + + expect(result).toBe(null); + + jest.useFakeTimers(); + }); +}); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index d0ab178296408..6ee12d97a6422 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -22,7 +22,7 @@ import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; -import { AbortError } from '../../../data/common'; +import { toPromise } from '../../../data/common/utils/abort_utils'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -38,6 +38,12 @@ import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; +const createAbortErrorValue = () => + createError({ + message: 'The expression was aborted.', + name: 'AbortError', + }); + export interface ExecutionParams< ExtraContext extends Record = Record > { @@ -70,7 +76,7 @@ export class Execution< /** * Dynamic state of the execution. */ - public readonly state: ExecutionContainer; + public readonly state: ExecutionContainer; /** * Initial input of the execution. @@ -91,6 +97,18 @@ export class Execution< */ private readonly abortController = new AbortController(); + /** + * Promise that rejects if/when abort controller sends "abort" signal. + */ + private readonly abortRejection = toPromise(this.abortController.signal, true); + + /** + * Races a given promise against the "abort" event of `abortController`. + */ + private race(promise: Promise): Promise { + return Promise.race([this.abortRejection, promise]); + } + /** * Whether .start() method has been called. */ @@ -99,7 +117,7 @@ export class Execution< /** * Future that tracks result or error of this execution. */ - private readonly firstResultFuture = new Defer(); + private readonly firstResultFuture = new Defer(); /** * Contract is a public representation of `Execution` instances. Contract we @@ -114,7 +132,7 @@ export class Execution< public readonly expression: string; - public get result(): Promise { + public get result(): Promise { return this.firstResultFuture.promise; } @@ -134,7 +152,7 @@ export class Execution< this.expression = params.expression || formatExpression(params.ast!); const ast = params.ast || parseExpression(this.expression); - this.state = createExecutionContainer({ + this.state = createExecutionContainer({ ...executor.state.get(), state: 'not-started', ast, @@ -173,7 +191,12 @@ export class Execution< this.state.transitions.start(); const { resolve, reject } = this.firstResultFuture; - this.invokeChain(this.state.get().ast.chain, input).then(resolve, reject); + const chainPromise = this.invokeChain(this.state.get().ast.chain, input); + + this.race(chainPromise).then(resolve, error => { + if (this.abortController.signal.aborted) resolve(createAbortErrorValue()); + else reject(error); + }); this.firstResultFuture.promise.then( result => { @@ -189,11 +212,6 @@ export class Execution< if (!chainArr.length) return input; for (const link of chainArr) { - // if execution was aborted return error - if (this.context.abortSignal && this.context.abortSignal.aborted) { - return createError(new AbortError('The expression was aborted.')); - } - const { function: fnName, arguments: fnArgs } = link; const fn = getByAlias(this.state.get().functions, fnName); @@ -207,10 +225,10 @@ export class Execution< try { // `resolveArgs` returns an object because the arguments themselves might // actually have a `then` function which would be treated as a `Promise`. - const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); + const { resolvedArgs } = await this.race(this.resolveArgs(fn, input, fnArgs)); args = resolvedArgs; timeStart = this.params.debug ? now() : 0; - const output = await this.invokeFunction(fn, input, resolvedArgs); + const output = await this.race(this.invokeFunction(fn, input, resolvedArgs)); if (this.params.debug) { const timeEnd: number = now(); @@ -256,7 +274,7 @@ export class Execution< args: Record ): Promise { const normalizedInput = this.cast(input, fn.inputTypes); - const output = await fn.fn(normalizedInput, args, this.context); + const output = await this.race(fn.fn(normalizedInput, args, this.context)); // Validate that the function returned the type it said it would. // This isn't required, but it keeps function developers honest. diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index 4b255a0f967b2..35554954d0828 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -31,7 +31,7 @@ export type ExpressionValueError = ExpressionValueBoxed< name?: string; stack?: string; }; - info: unknown; + info?: unknown; } >; diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 8236ff8709a82..bc27b0eda4959 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -17,9 +17,11 @@ * under the License. */ +import { ExpressionValueError } from '../../public'; + type ErrorLike = Partial>; -export const createError = (err: string | ErrorLike) => ({ +export const createError = (err: string | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -28,7 +30,7 @@ export const createError = (err: string | ErrorLike) => ({ : typeof err === 'object' ? err.stack : undefined, - message: typeof err === 'string' ? err : err.message, + message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', }, }); diff --git a/src/plugins/maps_legacy/public/__tests__/map/service_settings.js b/src/plugins/maps_legacy/public/__tests__/map/service_settings.js index 4cbe098501c67..822378163a7eb 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/service_settings.js +++ b/src/plugins/maps_legacy/public/__tests__/map/service_settings.js @@ -26,7 +26,7 @@ import EMS_TILES from './ems_mocks/sample_tiles.json'; import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright'; import EMS_STYLE_ROAD_MAP_DESATURATED from './ems_mocks/sample_style_desaturated'; import EMS_STYLE_DARK_MAP from './ems_mocks/sample_style_dark'; -import { ORIGIN } from '../../common/origin'; +import { ORIGIN } from '../../common/constants/origin'; describe('service_settings (FKA tilemaptest)', function() { let serviceSettings; diff --git a/src/plugins/maps_legacy/public/common/origin.ts b/src/plugins/maps_legacy/public/common/constants/origin.ts similarity index 100% rename from src/plugins/maps_legacy/public/common/origin.ts rename to src/plugins/maps_legacy/public/common/constants/origin.ts diff --git a/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts b/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts new file mode 100644 index 0000000000000..be9c4d0d9c37b --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TmsLayer } from '../../index'; +import { MapTypes } from './map_types'; + +export interface WMSOptions { + selectedTmsLayer?: TmsLayer; + enabled: boolean; + url?: string; + options: { + version?: string; + layers?: string; + format: string; + transparent: boolean; + attribution?: string; + styles?: string; + }; +} + +export interface TileMapVisParams { + colorSchema: string; + mapType: MapTypes; + isDesaturated: boolean; + addTooltip: boolean; + heatClusterSize: number; + legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft'; + mapZoom: number; + mapCenter: [number, number]; + wms: WMSOptions; +} diff --git a/src/plugins/maps_legacy/public/common/types/index.ts b/src/plugins/maps_legacy/public/common/types/index.ts new file mode 100644 index 0000000000000..e6cabdde82cd9 --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Use * syntax so that these exports do not break when internal + * types are stripped. + */ +export * from './external_basemap_types'; +export * from './map_types'; +export * from './region_map_types'; diff --git a/src/legacy/core_plugins/tile_map/public/map_types.ts b/src/plugins/maps_legacy/public/common/types/map_types.ts similarity index 100% rename from src/legacy/core_plugins/tile_map/public/map_types.ts rename to src/plugins/maps_legacy/public/common/types/map_types.ts diff --git a/src/plugins/maps_legacy/public/common/types/region_map_types.ts b/src/plugins/maps_legacy/public/common/types/region_map_types.ts new file mode 100644 index 0000000000000..0da597068f11e --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/region_map_types.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VectorLayer, FileLayerField } from '../../index'; +import { WMSOptions } from './external_basemap_types'; + +export interface RegionMapVisParams { + readonly addTooltip: true; + readonly legendPosition: 'bottomright'; + colorSchema: string; + emsHotLink?: string | null; + mapCenter: [number, number]; + mapZoom: number; + outlineWeight: number | ''; + isDisplayWarning: boolean; + showAllShapes: boolean; + selectedLayer?: VectorLayer; + selectedJoinField?: FileLayerField; + wms: WMSOptions; +} diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx similarity index 77% rename from src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx rename to src/plugins/maps_legacy/public/components/wms_internal_options.tsx index 47f5b8f31e62b..d1def8153d1a8 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx @@ -21,8 +21,8 @@ import React from 'react'; import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TextInputOption } from '../../../../../plugins/charts/public'; -import { WMSOptions } from '../types'; +import { TextInputOption } from '../../../charts/public'; +import { WMSOptions } from '../common/types/external_basemap_types'; interface WmsInternalOptions { wms: WMSOptions; @@ -32,14 +32,14 @@ interface WmsInternalOptions { function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { const wmsLink = ( - + ); const footnoteText = ( <> @@ -64,7 +64,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { @@ -74,14 +74,14 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -95,14 +95,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -117,7 +120,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { label={ <> @@ -126,7 +129,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { helpText={ <> {footnote} @@ -140,14 +143,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -161,13 +167,13 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { } helpText={ } @@ -179,14 +185,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx similarity index 82% rename from src/legacy/core_plugins/tile_map/public/components/wms_options.tsx rename to src/plugins/maps_legacy/public/components/wms_options.tsx index e74c260d3b8e5..4892463bb9f85 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_options.tsx @@ -21,12 +21,12 @@ import React, { useMemo } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TmsLayer } from '../../../../../plugins/maps_legacy/public'; -import { Vis } from '../../../../../plugins/visualizations/public'; -import { RegionMapVisParams } from '../../../region_map/public/types'; -import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; +import { TmsLayer } from '../index'; +import { Vis } from '../../../visualizations/public'; +import { RegionMapVisParams } from '../common/types/region_map_types'; +import { SelectOption, SwitchOption } from '../../../charts/public'; import { WmsInternalOptions } from './wms_internal_options'; -import { WMSOptions, TileMapVisParams } from '../types'; +import { WMSOptions, TileMapVisParams } from '../common/types/external_basemap_types'; interface Props { stateParams: TileMapVisParams | RegionMapVisParams; @@ -59,7 +59,7 @@ function WmsOptions({ stateParams, setValue, vis }: Props) {

@@ -67,10 +67,10 @@ function WmsOptions({ stateParams, setValue, vis }: Props) { new KibanaMap(...args); +} + +export function getBaseMapsVis(core: CoreSetup, serviceSettings: IServiceSettings) { + const getKibanaMap = getKibanaMapFactoryProvider(core); + return new BaseMapsVisualizationProvider(getKibanaMap, serviceSettings); +} + +export * from './common/types'; +export { ORIGIN } from './common/constants/origin'; + +export { WmsOptions } from './components/wms_options'; + export type MapsLegacyPluginSetup = ReturnType; export type MapsLegacyPluginStart = ReturnType; diff --git a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js similarity index 89% rename from src/legacy/core_plugins/tile_map/public/base_maps_visualization.js rename to src/plugins/maps_legacy/public/map/base_maps_visualization.js index 1dac4607280cc..c4ac671a5187c 100644 --- a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -19,16 +19,14 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMap } from '../../../../plugins/maps_legacy/public'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { toastNotifications } from 'ui/notify'; -import chrome from 'ui/chrome'; +import { getInjectedVarFunc, getUiSettings, getToasts } from '../kibana_services'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS -export function BaseMapsVisualizationProvider(mapServiceSettings, notificationService) { +export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) { /** * Abstract base class for a visualization consisting of a map with a single baselayer. * @class BaseMapsVisualization @@ -36,7 +34,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe */ const serviceSettings = mapServiceSettings; - const toastService = notificationService; + const toastService = getToasts(); return class BaseMapsVisualization { constructor(element, vis) { @@ -99,7 +97,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter; const services = { toastService }; - this._kibanaMap = new KibanaMap(this._container, options, services); + this._kibanaMap = getKibanaMap(this._container, options, services); this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default @@ -118,20 +116,20 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe _tmsConfigured() { const { wms } = this._getMapsParams(); - const hasTmsBaseLayer = !!wms.selectedTmsLayer; + const hasTmsBaseLayer = wms && !!wms.selectedTmsLayer; return hasTmsBaseLayer; } _wmsConfigured() { const { wms } = this._getMapsParams(); - const hasWmsBaseLayer = !!wms.enabled; + const hasWmsBaseLayer = wms && !!wms.enabled; return hasWmsBaseLayer; } async _updateBaseLayer() { - const emsTileLayerId = chrome.getInjected('emsTileLayerId', true); + const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId', true); if (!this._kibanaMap) { return; @@ -149,7 +147,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe this._setTmsLayer(initBasemapLayer); } } catch (e) { - toastNotifications.addWarning(e.message); + toastService.addWarning(e.message); return; } return; @@ -176,7 +174,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe this._setTmsLayer(selectedTmsLayer); } } catch (tmsLoadingError) { - toastNotifications.addWarning(tmsLoadingError.message); + toastService.addWarning(tmsLoadingError.message); } } @@ -190,7 +188,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe if (typeof isDesaturated !== 'boolean') { isDesaturated = true; } - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const isDarkMode = getUiSettings().get('theme:darkMode'); const meta = await serviceSettings.getAttributesForTMSLayer( tmsLayer, isDesaturated, @@ -208,7 +206,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe async _updateData() { throw new Error( - i18n.translate('tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage', { + i18n.translate('maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage', { defaultMessage: 'Child should implement this method to respond to data-update', }) ); diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index 1c4d0882cb7da..c7cec1b14159a 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -24,7 +24,8 @@ import $ from 'jquery'; import _ from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; -import { ORIGIN } from '../common/origin'; +import { ORIGIN } from '../common/constants/origin'; +import { getToasts } from '../kibana_services'; function makeFitControl(fitContainer, kibanaMap) { const FitControl = L.Control.extend({ @@ -101,7 +102,7 @@ function makeLegendControl(container, kibanaMap, position) { * Serves as simple abstraction for leaflet as well. */ export class KibanaMap extends EventEmitter { - constructor(containerNode, options, services) { + constructor(containerNode, options) { super(); this._containerNode = containerNode; this._leafletBaseLayer = null; @@ -116,7 +117,6 @@ export class KibanaMap extends EventEmitter { this._layers = []; this._listeners = []; this._showTooltip = false; - this.toastService = services ? services.toastService : null; const leafletOptions = { minZoom: options.minZoom, @@ -483,21 +483,19 @@ export class KibanaMap extends EventEmitter { } _addMaxZoomMessage = layer => { - if (this.toastService) { - const zoomWarningMsg = createZoomWarningMsg( - this.toastService, - this.getZoomLevel, - this.getMaxZoomLevel - ); + const zoomWarningMsg = createZoomWarningMsg( + getToasts(), + this.getZoomLevel, + this.getMaxZoomLevel + ); - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); + this._leafletMap.on('zoomend', zoomWarningMsg); + this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - } + layer.on('remove', () => { + this._leafletMap.off('zoomend', zoomWarningMsg); + this._containerNode.removeAttribute('data-test-subj'); + }); }; setLegendPosition(position) { diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index f4f0d66ee20de..8e3a0648e99d4 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -22,7 +22,7 @@ import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; import { getInjectedVarFunc } from '../kibana_services'; -import { ORIGIN } from '../common/origin'; +import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 751be65e1dbf6..acc7655a5e263 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -33,18 +33,20 @@ import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; * @public */ +export const bindSetupCoreAndPlugins = (core: CoreSetup) => { + setToasts(core.notifications.toasts); + setUiSettings(core.uiSettings); + setInjectedVarFunc(core.injectedMetadata.getInjectedVar); +}; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsLegacySetupDependencies {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsLegacyStartDependencies {} export class MapsLegacyPlugin implements Plugin { - constructor() {} - public setup(core: CoreSetup, plugins: MapsLegacySetupDependencies) { - setToasts(core.notifications.toasts); - setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + bindSetupCoreAndPlugins(core); return { serviceSettings: new ServiceSettings(), diff --git a/src/plugins/maps_legacy/public/tooltip_provider.js b/src/plugins/maps_legacy/public/tooltip_provider.js new file mode 100644 index 0000000000000..8563140816997 --- /dev/null +++ b/src/plugins/maps_legacy/public/tooltip_provider.js @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; + +function getToolTipContent(details) { + return ReactDOMServer.renderToStaticMarkup( + + + {details.map((detail, i) => ( + + + + + ))} + +
{detail.label}{detail.value}
+ ); +} + +export function mapTooltipProvider(element, formatter) { + return (...args) => { + const details = formatter(...args); + return details && getToolTipContent(details); + }; +} diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index fe3150fc0bb07..c1daf3445219f 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -17,23 +17,15 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route, useParams, useLocation } from 'react-router-dom'; -import { parse } from 'query-string'; -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { HashRouter, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, CoreStart, ChromeBreadcrumb, Capabilities } from 'src/core/public'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { CoreSetup, Capabilities } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; -import { DataPublicPluginStart } from '../../../data/public'; import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; -import { - ISavedObjectsManagementServiceRegistry, - SavedObjectsManagementActionServiceStart, -} from '../services'; -import { SavedObjectsTable } from './objects_table'; -import { SavedObjectEdition } from './object_view'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; import { getAllowedTypes } from './../lib'; interface MountParams { @@ -44,6 +36,8 @@ interface MountParams { let allowedObjectTypes: string[] | undefined; +const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); +const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ core, mountParams, @@ -63,23 +57,27 @@ export const mountManagementSection = async ({ - + }> + + - + }> + + @@ -103,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 0000000000000..5ac6e8e103c47 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { parse } from 'query-string'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; +import { SavedObjectEdition } from './object_view'; + +const SavedObjectsEditionPage = ({ + coreStart, + serviceRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const { service: serviceName, id } = useParams<{ service: string; id: string }>(); + const capabilities = coreStart.application.capabilities; + + const { search } = useLocation(); + const query = parse(search); + const service = serviceRegistry.get(serviceName); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + { + text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { + defaultMessage: 'Edit {savedObjectType}', + values: { savedObjectType: service?.service.type ?? 'object' }, + }), + }, + ]); + }, [setBreadcrumbs, service]); + + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SavedObjectsEditionPage as default }; diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx new file mode 100644 index 0000000000000..7660d17f91c5b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../services'; +import { SavedObjectsTable } from './objects_table'; + +const SavedObjectsTablePage = ({ + coreStart, + dataStart, + allowedTypes, + serviceRegistry, + actionRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + dataStart: DataPublicPluginStart; + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const capabilities = coreStart.application.capabilities; + const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + ]); + }, [setBreadcrumbs]); + + return ( + { + const { editUrl } = savedObject.meta; + if (editUrl) { + // previously, kbnUrl.change(object.meta.editUrl); was used. + // using direct access to location.hash seems the only option for now, + // as using react-router-dom will prefix the url with the router's basename + // which should be ignored there. + window.location.hash = editUrl; + } + }} + canGoInApp={savedObject => { + const { inAppUrl } = savedObject.meta; + return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; + }} + /> + ); +}; +// eslint-disable-next-line import/no-default-export +export { SavedObjectsTablePage as default }; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap new file mode 100644 index 0000000000000..8c0117e5a7266 --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -0,0 +1,492 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` + + + + + + +

+ +

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

+ } + /> + + +

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

+

+ + + +

+ , + "displayName": "Provide usage statistics", + "name": "telemetry:enabled", + "type": "boolean", + "value": true, + } + } + toasts={null} + /> +
+
+
+`; + +exports[`TelemetryManagementSectionComponent renders null because allowChangingOptInStatus is false 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent renders null because query does not match the SEARCH_TERMS 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `""`; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx new file mode 100644 index 0000000000000..d0c2bd13f802d --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -0,0 +1,284 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { TelemetryManagementSection } from './telemetry_management_section'; +import { TelemetryService } from '../../../telemetry/public/services'; +import { coreMock } from '../../../../core/public/mocks'; +import { telemetryManagementSectionWrapper } from './telemetry_management_section_wrapper'; + +describe('TelemetryManagementSectionComponent', () => { + const coreStart = coreMock.createStart(); + const coreSetup = coreMock.createSetup(); + + it('renders as expected', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + expect( + shallowWithIntl( + + ) + ).toMatchSnapshot(); + }); + + it('renders null because query does not match the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'asssdasdsad' } }) + ).toMatchSnapshot(); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders because query matches the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }).html() + ).not.toBe(''); // Renders something. + // I can't check against snapshot because of https://github.com/facebook/jest/issues/8618 + // expect(component).toMatchSnapshot(); + + // It should also render if there is no query at all. + expect(component.setProps({ ...component.props(), query: {} }).html()).not.toBe(''); + expect(onQueryMatchChange).toHaveBeenCalledWith(true); + + // Should only be called once because the second time does not change the result + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders null because allowChangingOptInStatus is false', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect(component).toMatchSnapshot(); + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + } finally { + component.unmount(); + } + }); + + it('shows the OptInExampleFlyout', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleExampleComponent = component.find('p > EuiLink[onClick]'); + const updatedView = toggleExampleComponent.simulate('click'); + updatedView.find('OptInExampleFlyout'); + updatedView.simulate('close'); + } finally { + component.unmount(); + } + }); + + it('toggles the OptIn button', async () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleOptInComponent = component.find('Field'); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(true); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(false); + telemetryService.setOptIn = jest.fn().mockRejectedValue(Error('test-error')); + await expect( + toggleOptInComponent.prop('handleChange')() + ).rejects.toStrictEqual(Error('test-error')); + } finally { + component.unmount(); + } + }); + + it('test the wrapper (for coverage purposes)', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + const Wrapper = telemetryManagementSectionWrapper(telemetryService); + expect( + shallowWithIntl( + + ).html() + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 26e075b666593..361c5ff719c54 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -69,7 +69,9 @@ export class TelemetryManagementSection extends Component { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); - const searchTermMatches = SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); + const searchTermMatches = + this.props.telemetryService.getCanChangeOptInStatus() && + SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); if (searchTermMatches !== this.state.queryMatches) { this.setState( diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts index d69b9bba31b03..e8404f918d609 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 7075e86eb56bf..9fdb8ccc919b7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -240,3 +240,7 @@ VisEditor.propTypes = { timeRange: PropTypes.object, appState: PropTypes.object, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VisEditor as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx new file mode 100644 index 0000000000000..d81bd95d8d771 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +// @ts-ignore +const VisEditorComponent = lazy(() => import('./vis_editor')); + +export const VisEditor = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index af50d3a06d1fc..f21b5f947bca7 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -21,7 +21,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { fetchIndexPatternFields } from './lib/fetch_fields'; import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; -import { VisEditor } from './components/vis_editor'; +import { VisEditor } from './components/vis_editor_lazy'; export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 008b13cce6565..b573225feaab1 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 c525ce7fa0b3b..2b0734ceb4d4d 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_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index c312705194cde..b52dcfbd914f9 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 e349cfbdc0024..f81f87d7ad2e1 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 bd6652a597203..895d496a896aa 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 0000000000000..5b3088b399ebf --- /dev/null +++ b/src/plugins/vis_type_vislib/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "visTypeVislib", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "optionalPlugins": ["visTypeXy"] +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap rename to src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/area.ts rename to src/plugins/vis_type_vislib/public/area.ts index 8a196da64fc4b..c42962ad50a4b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -39,7 +39,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts b/src/plugins/vis_type_vislib/public/components/common/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts rename to src/plugins/vis_type_vislib/public/components/common/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx b/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx rename to src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx b/src/plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx rename to src/plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/index.ts b/src/plugins/vis_type_vislib/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/index.ts rename to src/plugins/vis_type_vislib/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx new file mode 100644 index 0000000000000..0bd5694f71021 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SwitchOption, TextInputOption } from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; + +function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { + return ( + + +

+ +

+
+ + + + setGaugeValue('labels', { ...stateParams.gauge.labels, [paramName]: value }) + } + /> + + + + setGaugeValue('style', { ...stateParams.gauge.style, [paramName]: value }) + } + /> + + +
+ ); +} + +export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx index 433cc4edeb47b..c297fb08e4b68 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx @@ -29,8 +29,8 @@ import { SetColorRangeValue, SwitchOption, ColorSchemas, -} from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; +} from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; import { Gauge } from '../../../gauge'; function RangesPanel({ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx similarity index 91% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx index 48711de7d171a..b299b2e86ca40 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx @@ -22,9 +22,9 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption } from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; -import { AggGroupNames } from '../../../../../../../plugins/data/public'; +import { SelectOption } from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; +import { AggGroupNames } from '../../../../../data/public'; function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInternalProps) { const diasableAlignment = diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx new file mode 100644 index 0000000000000..7a89496d9441e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; + +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { + BasicOptions, + ColorRanges, + ColorSchemaOptions, + NumberInputOption, + SelectOption, + SwitchOption, + SetColorSchemaOptionsValue, + SetColorRangeValue, +} from '../../../../../charts/public'; +import { HeatmapVisParams } from '../../../heatmap'; +import { ValueAxis } from '../../../types'; +import { LabelsPanel } from './labels_panel'; + +function HeatmapOptions(props: VisOptionsProps) { + const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; + const [valueAxis] = stateParams.valueAxes; + const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; + const [isColorRangesValid, setIsColorRangesValid] = useState(false); + + const setValueAxisScale = useCallback( + (paramName: T, value: ValueAxis['scale'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + scale: { + ...valueAxis.scale, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + useEffect(() => { + setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid); + }, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]); + + return ( + <> + + +

+ +

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

+ +

+
+ + + + + + + + + + + + + + + + + {stateParams.setColorRange && ( + + )} +
+ + + + + + ); +} + +export { HeatmapOptions }; diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx new file mode 100644 index 0000000000000..8d5f529ce0fc7 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; + +import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { ValueAxis } from '../../../types'; +import { HeatmapVisParams } from '../../../heatmap'; +import { SwitchOption } from '../../../../../charts/public'; + +const VERTICAL_ROTATION = 270; + +interface LabelsPanelProps { + valueAxis: ValueAxis; + setValue: VisOptionsProps['setValue']; +} + +function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { + const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION; + + const setValueAxisLabels = useCallback( + (paramName: T, value: ValueAxis['labels'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + labels: { + ...valueAxis.labels, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + const setRotateLabels = useCallback( + (paramName: 'rotate', value: boolean) => + setValueAxisLabels(paramName, value ? VERTICAL_ROTATION : 0), + [setValueAxisLabels] + ); + + const setColor = useCallback(value => setValueAxisLabels('color', value), [setValueAxisLabels]); + + return ( + + +

+ +

+
+ + + + + + + + + + + +
+ ); +} + +export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/index.ts b/src/plugins/vis_type_vislib/public/components/options/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/index.ts rename to src/plugins/vis_type_vislib/public/components/options/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx index 91cdcd0f456b1..44ed0d5aeddab 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx @@ -25,8 +25,6 @@ import { Positions } from '../../../utils/collections'; import { LabelOptions } from './label_options'; import { categoryAxis, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('CategoryAxisPanel component', () => { let setCategoryAxis: jest.Mock; let onPositionChanged: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index 246c20a14807c..468fb1f8c315a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -25,7 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Axis } from '../../../types'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { Positions } from '../../../utils/collections'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx index c913fd4f35713..e2d4a0db9f1f9 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx @@ -25,8 +25,6 @@ import { LineOptions } from './line_options'; import { ChartTypes, ChartModes } from '../../../utils/collections'; import { valueAxis, seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('ChartOptions component', () => { let setParamByIndex: jest.Mock; let changeValueAxis: jest.Mock; diff --git a/src/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 89aab3a19c589..623a8d1f348e9 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 a93ee454a7afd..4798c67928f7f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx @@ -28,8 +28,6 @@ const DEFAULT_Y_EXTENTS = 'defaultYExtents'; const SCALE = 'scale'; const SET_Y_EXTENTS = 'setYExtents'; -jest.mock('ui/new_platform'); - describe('CustomExtentsOptions component', () => { let setValueAxis: jest.Mock; let setValueAxisScale: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx index a3a97df9e35ae..634d6b3f0641c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { ValueAxis } from '../../../types'; -import { NumberInputOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx index 48fcbdf8f9082..f500b7e58e9fd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx @@ -23,8 +23,6 @@ import { LabelOptions, LabelOptionsProps } from './label_options'; import { TruncateLabelsOption } from '../../common'; import { valueAxis } from './mocks'; -jest.mock('ui/new_platform'); - const FILTER = 'filter'; const ROTATE = 'rotate'; const DISABLED = 'disabled'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx index bc687e56646f6..14e1da6ebcc70 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Axis } from '../../../types'; import { TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; export type SetAxisLabel = ( paramName: T, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx index 5354bc9c033e6..e90c96146ec2c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx @@ -20,11 +20,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { LineOptions, LineOptionsParams } from './line_options'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption } from '../../../../../charts/public'; import { seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - const LINE_WIDTH = 'lineWidth'; const DRAW_LINES = 'drawLinesBetweenPoints'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx index 76f95bd93caf8..4b0cce94267f1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx @@ -22,13 +22,9 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { SeriesParam } from '../../../types'; -import { - NumberInputOption, - SelectOption, - SwitchOption, -} from '../../../../../../../plugins/charts/public'; +import { NumberInputOption, SelectOption, SwitchOption } from '../../../../../charts/public'; import { SetChart } from './chart_options'; export interface LineOptionsParams { diff --git a/src/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 a296281375dac..277fcf0cdbc3d 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 22a726b53363b..27c423860972c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx @@ -23,10 +23,10 @@ import { EuiPanel, EuiTitle, EuiSpacer, EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { ValueAxis, SeriesParam } from '../../../types'; import { ChartOptions } from './chart_options'; -import { SetParamByIndex, ChangeValueAxis } from './'; +import { SetParamByIndex, ChangeValueAxis } from '.'; export interface SeriesPanelProps { changeValueAxis: ChangeValueAxis; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx index 141273fa6bc3f..2f7dd4071b52c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx @@ -25,8 +25,6 @@ import { Positions } from '../../../utils/collections'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { valueAxis, seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('ValueAxesPanel component', () => { let setParamByIndex: jest.Mock; let onValueAxisPositionChanged: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx index 912c3b904b110..b17f67b81d2b0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx @@ -31,10 +31,10 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { SeriesParam, ValueAxis } from '../../../types'; import { ValueAxisOptions } from './value_axis_options'; -import { SetParamByIndex } from './'; +import { SetParamByIndex } from '.'; export interface ValueAxesPanelProps { isCategoryAxisHorizontal: boolean; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx index 876a6917ee0b4..1977bdba6eadf 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -21,13 +21,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; import { ValueAxis } from '../../../types'; -import { TextInputOption } from '../../../../../../../plugins/charts/public'; +import { TextInputOption } from '../../../../../charts/public'; import { LabelOptions } from './label_options'; import { ScaleTypes, Positions } from '../../../utils/collections'; import { valueAxis, vis } from './mocks'; -jest.mock('ui/new_platform'); - const POSITION = 'position'; interface PositionOption { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx index 1b89a766d0591..52962fe813b44 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx @@ -21,18 +21,14 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { ValueAxis } from '../../../types'; import { Positions } from '../../../utils/collections'; -import { - SelectOption, - SwitchOption, - TextInputOption, -} from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption, TextInputOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; import { isAxisHorizontal } from './utils'; -import { SetParamByIndex } from './'; +import { SetParamByIndex } from '.'; export type SetScale = ( paramName: T, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx index b5ed475ca8e31..3bacb0be34d13 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx @@ -21,9 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { YExtents, YExtentsProps } from './y_extents'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; - -jest.mock('ui/new_platform'); +import { NumberInputOption } from '../../../../../charts/public'; describe('YExtents component', () => { let setMultipleValidity: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx index faeb6069b5126..c2aa917dd3a6f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { Scale } from '../../../types'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption } from '../../../../../charts/public'; import { SetScale } from './value_axis_options'; const rangeError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage', { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/components/options/pie.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx rename to src/plugins/vis_type_vislib/public/components/options/pie.tsx index f6be9cd0bd8fe..54ba307982967 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/pie.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { TruncateLabelsOption } from '../common'; -import { BasicOptions, SwitchOption } from '../../../../../../plugins/charts/public'; +import { BasicOptions, SwitchOption } from '../../../../charts/public'; import { PieVisParams } from '../../pie'; function PieOptions(props: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx index 392d180d2c5f2..0126dce37c9f2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { BasicVislibParams, ValueAxis } from '../../../types'; function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/index.ts b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/index.ts rename to src/plugins/vis_type_vislib/public/components/options/point_series/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx index 903c1917751d9..60458b1f5c41f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ValidationVisOptionsProps } from '../../common'; -import { BasicOptions, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { BasicOptions, SwitchOption } from '../../../../../charts/public'; import { GridPanel } from './grid_panel'; import { ThresholdPanel } from './threshold_panel'; import { BasicVislibParams } from '../../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx index 12f058ec7dd1f..0823180300756 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx @@ -26,7 +26,7 @@ import { SelectOption, SwitchOption, RequiredNumberInputOption, -} from '../../../../../../../plugins/charts/public'; +} from '../../../../../charts/public'; import { BasicVislibParams } from '../../../types'; function ThresholdPanel({ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_normal.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_normal.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_normal.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_normal.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_percentage.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_percentage.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_percentage.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_percentage.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_d3.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_d3.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_d3.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_d3.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_data_point.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_data_point.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_data_point.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_data_point.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_config.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_config.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_config.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_config.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_d3.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_d3.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_d3.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_d3.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_data_point.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_data_point.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_data_point.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_data_point.json diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js new file mode 100644 index 0000000000000..ff8538021d275 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js @@ -0,0 +1,319 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + columns: [ + { + label: '200: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826600000, + y: 4, + }, + { + x: 1415826630000, + y: 8, + }, + { + x: 1415826660000, + y: 7, + }, + { + x: 1415826690000, + y: 5, + }, + { + x: 1415826720000, + y: 5, + }, + { + x: 1415826750000, + y: 4, + }, + { + x: 1415826780000, + y: 10, + }, + { + x: 1415826810000, + y: 7, + }, + { + x: 1415826840000, + y: 9, + }, + { + x: 1415826870000, + y: 8, + }, + { + x: 1415826900000, + y: 9, + }, + { + x: 1415826930000, + y: 8, + }, + { + x: 1415826960000, + y: 3, + }, + { + x: 1415826990000, + y: 9, + }, + { + x: 1415827020000, + y: 6, + }, + { + x: 1415827050000, + y: 8, + }, + { + x: 1415827080000, + y: 7, + }, + { + x: 1415827110000, + y: 4, + }, + { + x: 1415827140000, + y: 6, + }, + { + x: 1415827170000, + y: 10, + }, + { + x: 1415827200000, + y: 2, + }, + { + x: 1415827230000, + y: 8, + }, + { + x: 1415827260000, + y: 5, + }, + { + x: 1415827290000, + y: 6, + }, + { + x: 1415827320000, + y: 6, + }, + { + x: 1415827350000, + y: 10, + }, + { + x: 1415827380000, + y: 6, + }, + { + x: 1415827410000, + y: 6, + }, + { + x: 1415827440000, + y: 12, + }, + { + x: 1415827470000, + y: 9, + }, + { + x: 1415827500000, + y: 1, + }, + ], + }, + ], + }, + { + label: '503: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826630000, + y: 1, + }, + { + x: 1415826660000, + y: 1, + }, + { + x: 1415826720000, + y: 1, + }, + { + x: 1415826780000, + y: 1, + }, + { + x: 1415826900000, + y: 1, + }, + { + x: 1415827020000, + y: 1, + }, + { + x: 1415827080000, + y: 1, + }, + { + x: 1415827110000, + y: 2, + }, + ], + }, + ], + }, + { + label: '404: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826660000, + y: 1, + }, + { + x: 1415826720000, + y: 1, + }, + { + x: 1415826810000, + y: 1, + }, + { + x: 1415826960000, + y: 1, + }, + { + x: 1415827050000, + y: 1, + }, + { + x: 1415827260000, + y: 1, + }, + { + x: 1415827380000, + y: 1, + }, + { + x: 1415827410000, + y: 1, + }, + ], + }, + ], + }, + ], + xAxisOrderedValues: [ + 1415826600000, + 1415826630000, + 1415826660000, + 1415826690000, + 1415826720000, + 1415826750000, + 1415826780000, + 1415826810000, + 1415826840000, + 1415826870000, + 1415826900000, + 1415826930000, + 1415826960000, + 1415826990000, + 1415827020000, + 1415827050000, + 1415827080000, + 1415827110000, + 1415827140000, + 1415827170000, + 1415827200000, + 1415827230000, + 1415827260000, + 1415827290000, + 1415827320000, + 1415827350000, + 1415827380000, + 1415827410000, + 1415827440000, + 1415827470000, + 1415827500000, + ], + hits: 225, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js new file mode 100644 index 0000000000000..6367197acdece --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js @@ -0,0 +1,1697 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + rows: [ + { + label: '0.0-1000.0: bytes', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826260456, + max: 1415827160456, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'jpg', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 1, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 1, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 1, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 1, + y0: 0, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 1, + y0: 0, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 1, + y0: 0, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 1, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 1, + }, + { + x: 1415827110000, + y: 1, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 1, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 1, + }, + { + x: 1415827110000, + y: 0, + y0: 2, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 1, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 1, + y0: 0, + }, + { + x: 1415826480000, + y: 1, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 3, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 1, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 1, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 1, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 1, + y0: 1, + }, + { + x: 1415827110000, + y: 1, + y0: 2, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + ], + }, + { + label: '1000.0-2000.0: bytes', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826260457, + max: 1415827160457, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'jpg', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 1, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 1, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 2, + y0: 0, + }, + { + x: 1415826840000, + y: 1, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 1, + y0: 0, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 1, + y0: 0, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 1, + y0: 0, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 1, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 1, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 1, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 1, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 1, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + ], + }, + ], + xAxisOrderedValues: [ + 1415826240000, + 1415826270000, + 1415826300000, + 1415826330000, + 1415826360000, + 1415826390000, + 1415826420000, + 1415826450000, + 1415826480000, + 1415826510000, + 1415826540000, + 1415826570000, + 1415826600000, + 1415826630000, + 1415826660000, + 1415826690000, + 1415826720000, + 1415826750000, + 1415826780000, + 1415826810000, + 1415826840000, + 1415826870000, + 1415826900000, + 1415826930000, + 1415826960000, + 1415826990000, + 1415827020000, + 1415827050000, + 1415827080000, + 1415827110000, + 1415827140000, + ], + hits: 236, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js new file mode 100644 index 0000000000000..ba0d8bf251c6f --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export const rowsSeriesWithHoles = { + rows: [ + { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761510000, + y: 22, + }, + { + x: 1411761540000, + y: 17, + }, + { + x: 1411761840000, + y: 20, + }, + { + x: 1411761870000, + y: 20, + }, + { + x: 1411761900000, + y: 21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: 13, + }, + { + x: 1411762020000, + y: 14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: 17, + }, + { + x: 1411762110000, + y: 14, + }, + { + x: 1411762140000, + y: 22, + }, + { + x: 1411762170000, + y: 14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762320000, + y: 15, + }, + { + x: 1411762350000, + y: 4, + }, + ], + }, + ], + hits: 533, + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 1411761450000, + 1411761510000, + 1411761540000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762320000, + 1411762350000, + ], +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js new file mode 100644 index 0000000000000..89e4f9a32cee1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761480000, + y: 18, + }, + { + x: 1411761510000, + y: 22, + }, + { + x: 1411761540000, + y: 17, + }, + { + x: 1411761570000, + y: 17, + }, + { + x: 1411761600000, + y: 21, + }, + { + x: 1411761630000, + y: 16, + }, + { + x: 1411761660000, + y: 17, + }, + { + x: 1411761690000, + y: 15, + }, + { + x: 1411761720000, + y: 19, + }, + { + x: 1411761750000, + y: 11, + }, + { + x: 1411761780000, + y: 13, + }, + { + x: 1411761810000, + y: 24, + }, + { + x: 1411761840000, + y: 20, + }, + { + x: 1411761870000, + y: 20, + }, + { + x: 1411761900000, + y: 21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: 13, + }, + { + x: 1411762020000, + y: 14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: 17, + }, + { + x: 1411762110000, + y: 14, + }, + { + x: 1411762140000, + y: 22, + }, + { + x: 1411762170000, + y: 14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762230000, + y: 22, + }, + { + x: 1411762260000, + y: 17, + }, + { + x: 1411762290000, + y: 8, + }, + { + x: 1411762320000, + y: 15, + }, + { + x: 1411762350000, + y: 4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js new file mode 100644 index 0000000000000..85078a2ec15af --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export const seriesMonthlyInterval = { + label: '', + xAxisLabel: '@timestamp per month', + ordered: { + date: true, + min: 1451631600000, + max: 1483254000000, + interval: 2678000000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1451631600000, + y: 10220, + }, + { + x: 1454310000000, + y: 9997, + }, + { + x: 1456815600000, + y: 10792, + }, + { + x: 1459490400000, + y: 10262, + }, + { + x: 1462082400000, + y: 10080, + }, + { + x: 1464760800000, + y: 11161, + }, + { + x: 1467352800000, + y: 9933, + }, + { + x: 1470031200000, + y: 10342, + }, + { + x: 1472709600000, + y: 10887, + }, + { + x: 1475301600000, + y: 9666, + }, + { + x: 1477980000000, + y: 9556, + }, + { + x: 1480575600000, + y: 11644, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1451631600000, + 1454310000000, + 1456815600000, + 1459490400000, + 1462082400000, + 1464760800000, + 1467352800000, + 1470031200000, + 1472709600000, + 1475301600000, + 1477980000000, + 1480575600000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js new file mode 100644 index 0000000000000..821c04685d22e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: -41, + }, + { + x: 1411761480000, + y: -18, + }, + { + x: 1411761510000, + y: -22, + }, + { + x: 1411761540000, + y: -17, + }, + { + x: 1411761570000, + y: -17, + }, + { + x: 1411761600000, + y: -21, + }, + { + x: 1411761630000, + y: -16, + }, + { + x: 1411761660000, + y: -17, + }, + { + x: 1411761690000, + y: -15, + }, + { + x: 1411761720000, + y: -19, + }, + { + x: 1411761750000, + y: -11, + }, + { + x: 1411761780000, + y: -13, + }, + { + x: 1411761810000, + y: -24, + }, + { + x: 1411761840000, + y: -20, + }, + { + x: 1411761870000, + y: -20, + }, + { + x: 1411761900000, + y: -21, + }, + { + x: 1411761930000, + y: -17, + }, + { + x: 1411761960000, + y: -20, + }, + { + x: 1411761990000, + y: -13, + }, + { + x: 1411762020000, + y: -14, + }, + { + x: 1411762050000, + y: -25, + }, + { + x: 1411762080000, + y: -17, + }, + { + x: 1411762110000, + y: -14, + }, + { + x: 1411762140000, + y: -22, + }, + { + x: 1411762170000, + y: -14, + }, + { + x: 1411762200000, + y: -19, + }, + { + x: 1411762230000, + y: -22, + }, + { + x: 1411762260000, + y: -17, + }, + { + x: 1411762290000, + y: -8, + }, + { + x: 1411762320000, + y: -15, + }, + { + x: 1411762350000, + y: -4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js new file mode 100644 index 0000000000000..65821ac58eb0d --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761480000, + y: 18, + }, + { + x: 1411761510000, + y: -22, + }, + { + x: 1411761540000, + y: -17, + }, + { + x: 1411761570000, + y: -17, + }, + { + x: 1411761600000, + y: -21, + }, + { + x: 1411761630000, + y: -16, + }, + { + x: 1411761660000, + y: 17, + }, + { + x: 1411761690000, + y: 15, + }, + { + x: 1411761720000, + y: 19, + }, + { + x: 1411761750000, + y: 11, + }, + { + x: 1411761780000, + y: -13, + }, + { + x: 1411761810000, + y: -24, + }, + { + x: 1411761840000, + y: -20, + }, + { + x: 1411761870000, + y: -20, + }, + { + x: 1411761900000, + y: -21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: -13, + }, + { + x: 1411762020000, + y: -14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: -17, + }, + { + x: 1411762110000, + y: -14, + }, + { + x: 1411762140000, + y: -22, + }, + { + x: 1411762170000, + y: -14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762230000, + y: 22, + }, + { + x: 1411762260000, + y: 17, + }, + { + x: 1411762290000, + y: 8, + }, + { + x: 1411762320000, + y: -15, + }, + { + x: 1411762350000, + y: -4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js new file mode 100644 index 0000000000000..b6f731c9655d4 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js @@ -0,0 +1,1576 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 10 min', + ordered: { + date: true, + min: 1413544140087, + max: 1413587340087, + interval: 600000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'html', + values: [ + { + x: 1413543600000, + y: 140, + }, + { + x: 1413544200000, + y: 1388, + }, + { + x: 1413544800000, + y: 1308, + }, + { + x: 1413545400000, + y: 1356, + }, + { + x: 1413546000000, + y: 1314, + }, + { + x: 1413546600000, + y: 1343, + }, + { + x: 1413547200000, + y: 1353, + }, + { + x: 1413547800000, + y: 1353, + }, + { + x: 1413548400000, + y: 1334, + }, + { + x: 1413549000000, + y: 1433, + }, + { + x: 1413549600000, + y: 1331, + }, + { + x: 1413550200000, + y: 1349, + }, + { + x: 1413550800000, + y: 1323, + }, + { + x: 1413551400000, + y: 1203, + }, + { + x: 1413552000000, + y: 1231, + }, + { + x: 1413552600000, + y: 1227, + }, + { + x: 1413553200000, + y: 1187, + }, + { + x: 1413553800000, + y: 1119, + }, + { + x: 1413554400000, + y: 1159, + }, + { + x: 1413555000000, + y: 1117, + }, + { + x: 1413555600000, + y: 1152, + }, + { + x: 1413556200000, + y: 1057, + }, + { + x: 1413556800000, + y: 1009, + }, + { + x: 1413557400000, + y: 979, + }, + { + x: 1413558000000, + y: 975, + }, + { + x: 1413558600000, + y: 848, + }, + { + x: 1413559200000, + y: 873, + }, + { + x: 1413559800000, + y: 808, + }, + { + x: 1413560400000, + y: 784, + }, + { + x: 1413561000000, + y: 799, + }, + { + x: 1413561600000, + y: 684, + }, + { + x: 1413562200000, + y: 727, + }, + { + x: 1413562800000, + y: 621, + }, + { + x: 1413563400000, + y: 615, + }, + { + x: 1413564000000, + y: 569, + }, + { + x: 1413564600000, + y: 523, + }, + { + x: 1413565200000, + y: 474, + }, + { + x: 1413565800000, + y: 470, + }, + { + x: 1413566400000, + y: 466, + }, + { + x: 1413567000000, + y: 394, + }, + { + x: 1413567600000, + y: 404, + }, + { + x: 1413568200000, + y: 389, + }, + { + x: 1413568800000, + y: 312, + }, + { + x: 1413569400000, + y: 274, + }, + { + x: 1413570000000, + y: 285, + }, + { + x: 1413570600000, + y: 299, + }, + { + x: 1413571200000, + y: 207, + }, + { + x: 1413571800000, + y: 213, + }, + { + x: 1413572400000, + y: 119, + }, + { + x: 1413573600000, + y: 122, + }, + { + x: 1413574200000, + y: 169, + }, + { + x: 1413574800000, + y: 151, + }, + { + x: 1413575400000, + y: 152, + }, + { + x: 1413576000000, + y: 115, + }, + { + x: 1413576600000, + y: 117, + }, + { + x: 1413577200000, + y: 108, + }, + { + x: 1413577800000, + y: 100, + }, + { + x: 1413578400000, + y: 78, + }, + { + x: 1413579000000, + y: 88, + }, + { + x: 1413579600000, + y: 63, + }, + { + x: 1413580200000, + y: 58, + }, + { + x: 1413580800000, + y: 45, + }, + { + x: 1413581400000, + y: 57, + }, + { + x: 1413582000000, + y: 34, + }, + { + x: 1413582600000, + y: 41, + }, + { + x: 1413583200000, + y: 24, + }, + { + x: 1413583800000, + y: 27, + }, + { + x: 1413584400000, + y: 19, + }, + { + x: 1413585000000, + y: 24, + }, + { + x: 1413585600000, + y: 18, + }, + { + x: 1413586200000, + y: 17, + }, + { + x: 1413586800000, + y: 14, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1413543600000, + y: 90, + }, + { + x: 1413544200000, + y: 949, + }, + { + x: 1413544800000, + y: 1012, + }, + { + x: 1413545400000, + y: 1027, + }, + { + x: 1413546000000, + y: 1073, + }, + { + x: 1413546600000, + y: 992, + }, + { + x: 1413547200000, + y: 1005, + }, + { + x: 1413547800000, + y: 1014, + }, + { + x: 1413548400000, + y: 987, + }, + { + x: 1413549000000, + y: 982, + }, + { + x: 1413549600000, + y: 1086, + }, + { + x: 1413550200000, + y: 998, + }, + { + x: 1413550800000, + y: 935, + }, + { + x: 1413551400000, + y: 995, + }, + { + x: 1413552000000, + y: 926, + }, + { + x: 1413552600000, + y: 897, + }, + { + x: 1413553200000, + y: 873, + }, + { + x: 1413553800000, + y: 885, + }, + { + x: 1413554400000, + y: 859, + }, + { + x: 1413555000000, + y: 852, + }, + { + x: 1413555600000, + y: 779, + }, + { + x: 1413556200000, + y: 739, + }, + { + x: 1413556800000, + y: 783, + }, + { + x: 1413557400000, + y: 784, + }, + { + x: 1413558000000, + y: 687, + }, + { + x: 1413558600000, + y: 660, + }, + { + x: 1413559200000, + y: 672, + }, + { + x: 1413559800000, + y: 600, + }, + { + x: 1413560400000, + y: 659, + }, + { + x: 1413561000000, + y: 540, + }, + { + x: 1413561600000, + y: 539, + }, + { + x: 1413562200000, + y: 481, + }, + { + x: 1413562800000, + y: 498, + }, + { + x: 1413563400000, + y: 444, + }, + { + x: 1413564000000, + y: 452, + }, + { + x: 1413564600000, + y: 408, + }, + { + x: 1413565200000, + y: 358, + }, + { + x: 1413565800000, + y: 321, + }, + { + x: 1413566400000, + y: 305, + }, + { + x: 1413567000000, + y: 292, + }, + { + x: 1413567600000, + y: 289, + }, + { + x: 1413568200000, + y: 239, + }, + { + x: 1413568800000, + y: 256, + }, + { + x: 1413569400000, + y: 220, + }, + { + x: 1413570000000, + y: 205, + }, + { + x: 1413570600000, + y: 201, + }, + { + x: 1413571200000, + y: 183, + }, + { + x: 1413571800000, + y: 172, + }, + { + x: 1413572400000, + y: 73, + }, + { + x: 1413573600000, + y: 90, + }, + { + x: 1413574200000, + y: 130, + }, + { + x: 1413574800000, + y: 104, + }, + { + x: 1413575400000, + y: 108, + }, + { + x: 1413576000000, + y: 92, + }, + { + x: 1413576600000, + y: 79, + }, + { + x: 1413577200000, + y: 90, + }, + { + x: 1413577800000, + y: 72, + }, + { + x: 1413578400000, + y: 68, + }, + { + x: 1413579000000, + y: 52, + }, + { + x: 1413579600000, + y: 60, + }, + { + x: 1413580200000, + y: 51, + }, + { + x: 1413580800000, + y: 32, + }, + { + x: 1413581400000, + y: 37, + }, + { + x: 1413582000000, + y: 30, + }, + { + x: 1413582600000, + y: 29, + }, + { + x: 1413583200000, + y: 24, + }, + { + x: 1413583800000, + y: 16, + }, + { + x: 1413584400000, + y: 15, + }, + { + x: 1413585000000, + y: 15, + }, + { + x: 1413585600000, + y: 10, + }, + { + x: 1413586200000, + y: 9, + }, + { + x: 1413586800000, + y: 9, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1413543600000, + y: 44, + }, + { + x: 1413544200000, + y: 495, + }, + { + x: 1413544800000, + y: 489, + }, + { + x: 1413545400000, + y: 492, + }, + { + x: 1413546000000, + y: 556, + }, + { + x: 1413546600000, + y: 536, + }, + { + x: 1413547200000, + y: 511, + }, + { + x: 1413547800000, + y: 479, + }, + { + x: 1413548400000, + y: 544, + }, + { + x: 1413549000000, + y: 513, + }, + { + x: 1413549600000, + y: 501, + }, + { + x: 1413550200000, + y: 532, + }, + { + x: 1413550800000, + y: 440, + }, + { + x: 1413551400000, + y: 455, + }, + { + x: 1413552000000, + y: 455, + }, + { + x: 1413552600000, + y: 471, + }, + { + x: 1413553200000, + y: 428, + }, + { + x: 1413553800000, + y: 457, + }, + { + x: 1413554400000, + y: 450, + }, + { + x: 1413555000000, + y: 418, + }, + { + x: 1413555600000, + y: 398, + }, + { + x: 1413556200000, + y: 397, + }, + { + x: 1413556800000, + y: 359, + }, + { + x: 1413557400000, + y: 398, + }, + { + x: 1413558000000, + y: 339, + }, + { + x: 1413558600000, + y: 363, + }, + { + x: 1413559200000, + y: 297, + }, + { + x: 1413559800000, + y: 323, + }, + { + x: 1413560400000, + y: 302, + }, + { + x: 1413561000000, + y: 260, + }, + { + x: 1413561600000, + y: 276, + }, + { + x: 1413562200000, + y: 249, + }, + { + x: 1413562800000, + y: 248, + }, + { + x: 1413563400000, + y: 235, + }, + { + x: 1413564000000, + y: 234, + }, + { + x: 1413564600000, + y: 188, + }, + { + x: 1413565200000, + y: 192, + }, + { + x: 1413565800000, + y: 173, + }, + { + x: 1413566400000, + y: 160, + }, + { + x: 1413567000000, + y: 137, + }, + { + x: 1413567600000, + y: 158, + }, + { + x: 1413568200000, + y: 111, + }, + { + x: 1413568800000, + y: 145, + }, + { + x: 1413569400000, + y: 118, + }, + { + x: 1413570000000, + y: 104, + }, + { + x: 1413570600000, + y: 80, + }, + { + x: 1413571200000, + y: 79, + }, + { + x: 1413571800000, + y: 86, + }, + { + x: 1413572400000, + y: 47, + }, + { + x: 1413573600000, + y: 49, + }, + { + x: 1413574200000, + y: 68, + }, + { + x: 1413574800000, + y: 78, + }, + { + x: 1413575400000, + y: 77, + }, + { + x: 1413576000000, + y: 50, + }, + { + x: 1413576600000, + y: 51, + }, + { + x: 1413577200000, + y: 40, + }, + { + x: 1413577800000, + y: 42, + }, + { + x: 1413578400000, + y: 29, + }, + { + x: 1413579000000, + y: 24, + }, + { + x: 1413579600000, + y: 30, + }, + { + x: 1413580200000, + y: 18, + }, + { + x: 1413580800000, + y: 15, + }, + { + x: 1413581400000, + y: 19, + }, + { + x: 1413582000000, + y: 18, + }, + { + x: 1413582600000, + y: 13, + }, + { + x: 1413583200000, + y: 11, + }, + { + x: 1413583800000, + y: 11, + }, + { + x: 1413584400000, + y: 13, + }, + { + x: 1413585000000, + y: 9, + }, + { + x: 1413585600000, + y: 9, + }, + { + x: 1413586200000, + y: 9, + }, + { + x: 1413586800000, + y: 3, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1413543600000, + y: 35, + }, + { + x: 1413544200000, + y: 360, + }, + { + x: 1413544800000, + y: 343, + }, + { + x: 1413545400000, + y: 329, + }, + { + x: 1413546000000, + y: 345, + }, + { + x: 1413546600000, + y: 336, + }, + { + x: 1413547200000, + y: 330, + }, + { + x: 1413547800000, + y: 334, + }, + { + x: 1413548400000, + y: 326, + }, + { + x: 1413549000000, + y: 351, + }, + { + x: 1413549600000, + y: 334, + }, + { + x: 1413550200000, + y: 351, + }, + { + x: 1413550800000, + y: 337, + }, + { + x: 1413551400000, + y: 306, + }, + { + x: 1413552000000, + y: 346, + }, + { + x: 1413552600000, + y: 317, + }, + { + x: 1413553200000, + y: 298, + }, + { + x: 1413553800000, + y: 288, + }, + { + x: 1413554400000, + y: 283, + }, + { + x: 1413555000000, + y: 262, + }, + { + x: 1413555600000, + y: 245, + }, + { + x: 1413556200000, + y: 259, + }, + { + x: 1413556800000, + y: 267, + }, + { + x: 1413557400000, + y: 230, + }, + { + x: 1413558000000, + y: 218, + }, + { + x: 1413558600000, + y: 241, + }, + { + x: 1413559200000, + y: 213, + }, + { + x: 1413559800000, + y: 239, + }, + { + x: 1413560400000, + y: 208, + }, + { + x: 1413561000000, + y: 187, + }, + { + x: 1413561600000, + y: 166, + }, + { + x: 1413562200000, + y: 154, + }, + { + x: 1413562800000, + y: 184, + }, + { + x: 1413563400000, + y: 148, + }, + { + x: 1413564000000, + y: 153, + }, + { + x: 1413564600000, + y: 149, + }, + { + x: 1413565200000, + y: 102, + }, + { + x: 1413565800000, + y: 110, + }, + { + x: 1413566400000, + y: 121, + }, + { + x: 1413567000000, + y: 120, + }, + { + x: 1413567600000, + y: 86, + }, + { + x: 1413568200000, + y: 96, + }, + { + x: 1413568800000, + y: 71, + }, + { + x: 1413569400000, + y: 92, + }, + { + x: 1413570000000, + y: 65, + }, + { + x: 1413570600000, + y: 54, + }, + { + x: 1413571200000, + y: 68, + }, + { + x: 1413571800000, + y: 57, + }, + { + x: 1413572400000, + y: 33, + }, + { + x: 1413573600000, + y: 47, + }, + { + x: 1413574200000, + y: 42, + }, + { + x: 1413574800000, + y: 39, + }, + { + x: 1413575400000, + y: 25, + }, + { + x: 1413576000000, + y: 31, + }, + { + x: 1413576600000, + y: 37, + }, + { + x: 1413577200000, + y: 35, + }, + { + x: 1413577800000, + y: 19, + }, + { + x: 1413578400000, + y: 15, + }, + { + x: 1413579000000, + y: 21, + }, + { + x: 1413579600000, + y: 16, + }, + { + x: 1413580200000, + y: 18, + }, + { + x: 1413580800000, + y: 10, + }, + { + x: 1413581400000, + y: 13, + }, + { + x: 1413582000000, + y: 14, + }, + { + x: 1413582600000, + y: 11, + }, + { + x: 1413583200000, + y: 4, + }, + { + x: 1413583800000, + y: 6, + }, + { + x: 1413584400000, + y: 3, + }, + { + x: 1413585000000, + y: 6, + }, + { + x: 1413585600000, + y: 6, + }, + { + x: 1413586200000, + y: 2, + }, + { + x: 1413586800000, + y: 3, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1413543600000, + y: 21, + }, + { + x: 1413544200000, + y: 191, + }, + { + x: 1413544800000, + y: 176, + }, + { + x: 1413545400000, + y: 166, + }, + { + x: 1413546000000, + y: 183, + }, + { + x: 1413546600000, + y: 170, + }, + { + x: 1413547200000, + y: 153, + }, + { + x: 1413547800000, + y: 202, + }, + { + x: 1413548400000, + y: 175, + }, + { + x: 1413549000000, + y: 161, + }, + { + x: 1413549600000, + y: 174, + }, + { + x: 1413550200000, + y: 167, + }, + { + x: 1413550800000, + y: 171, + }, + { + x: 1413551400000, + y: 176, + }, + { + x: 1413552000000, + y: 139, + }, + { + x: 1413552600000, + y: 145, + }, + { + x: 1413553200000, + y: 157, + }, + { + x: 1413553800000, + y: 148, + }, + { + x: 1413554400000, + y: 149, + }, + { + x: 1413555000000, + y: 135, + }, + { + x: 1413555600000, + y: 118, + }, + { + x: 1413556200000, + y: 142, + }, + { + x: 1413556800000, + y: 141, + }, + { + x: 1413557400000, + y: 146, + }, + { + x: 1413558000000, + y: 114, + }, + { + x: 1413558600000, + y: 115, + }, + { + x: 1413559200000, + y: 136, + }, + { + x: 1413559800000, + y: 106, + }, + { + x: 1413560400000, + y: 92, + }, + { + x: 1413561000000, + y: 97, + }, + { + x: 1413561600000, + y: 90, + }, + { + x: 1413562200000, + y: 69, + }, + { + x: 1413562800000, + y: 66, + }, + { + x: 1413563400000, + y: 93, + }, + { + x: 1413564000000, + y: 75, + }, + { + x: 1413564600000, + y: 68, + }, + { + x: 1413565200000, + y: 55, + }, + { + x: 1413565800000, + y: 73, + }, + { + x: 1413566400000, + y: 57, + }, + { + x: 1413567000000, + y: 48, + }, + { + x: 1413567600000, + y: 41, + }, + { + x: 1413568200000, + y: 39, + }, + { + x: 1413568800000, + y: 32, + }, + { + x: 1413569400000, + y: 33, + }, + { + x: 1413570000000, + y: 39, + }, + { + x: 1413570600000, + y: 35, + }, + { + x: 1413571200000, + y: 25, + }, + { + x: 1413571800000, + y: 28, + }, + { + x: 1413572400000, + y: 8, + }, + { + x: 1413573600000, + y: 13, + }, + { + x: 1413574200000, + y: 23, + }, + { + x: 1413574800000, + y: 19, + }, + { + x: 1413575400000, + y: 16, + }, + { + x: 1413576000000, + y: 22, + }, + { + x: 1413576600000, + y: 13, + }, + { + x: 1413577200000, + y: 21, + }, + { + x: 1413577800000, + y: 11, + }, + { + x: 1413578400000, + y: 12, + }, + { + x: 1413579000000, + y: 10, + }, + { + x: 1413579600000, + y: 7, + }, + { + x: 1413580200000, + y: 4, + }, + { + x: 1413580800000, + y: 5, + }, + { + x: 1413581400000, + y: 7, + }, + { + x: 1413582000000, + y: 9, + }, + { + x: 1413582600000, + y: 2, + }, + { + x: 1413583200000, + y: 2, + }, + { + x: 1413583800000, + y: 4, + }, + { + x: 1413584400000, + y: 6, + }, + { + x: 1413585600000, + y: 2, + }, + { + x: 1413586200000, + y: 4, + }, + { + x: 1413586800000, + y: 4, + }, + ], + }, + ], + hits: 108970, + xAxisOrderedValues: [ + 1413543600000, + 1413544200000, + 1413544800000, + 1413545400000, + 1413546000000, + 1413546600000, + 1413547200000, + 1413547800000, + 1413548400000, + 1413549000000, + 1413549600000, + 1413550200000, + 1413550800000, + 1413551400000, + 1413552000000, + 1413552600000, + 1413553200000, + 1413553800000, + 1413554400000, + 1413555000000, + 1413555600000, + 1413556200000, + 1413556800000, + 1413557400000, + 1413558000000, + 1413558600000, + 1413559200000, + 1413559800000, + 1413560400000, + 1413561000000, + 1413561600000, + 1413562200000, + 1413562800000, + 1413563400000, + 1413564000000, + 1413564600000, + 1413565200000, + 1413565800000, + 1413566400000, + 1413567000000, + 1413567600000, + 1413568200000, + 1413568800000, + 1413569400000, + 1413570000000, + 1413570600000, + 1413571200000, + 1413571800000, + 1413572400000, + 1413573600000, + 1413574200000, + 1413574800000, + 1413575400000, + 1413576000000, + 1413576600000, + 1413577200000, + 1413577800000, + 1413578400000, + 1413579000000, + 1413579600000, + 1413580200000, + 1413580800000, + 1413581400000, + 1413582000000, + 1413582600000, + 1413583200000, + 1413583800000, + 1413584400000, + 1413585000000, + 1413585600000, + 1413586200000, + 1413586800000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js new file mode 100644 index 0000000000000..8144a996e3424 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + label: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 10379, + }, + { + x: 'png', + y: 6395, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 9253, + }, + { + x: 'png', + y: 5571, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 7740, + }, + { + x: 'png', + y: 4697, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171443, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js new file mode 100644 index 0000000000000..e783246972e4a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + label: '200: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 25260, + }, + { + x: 'png', + y: 15311, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '404: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 1352, + }, + { + x: 'png', + y: 826, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 761, + }, + { + x: 'png', + y: 527, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171443, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js new file mode 100644 index 0000000000000..71ee039f98938 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 27374, + }, + { + x: 'html', + y: 0, + }, + { + x: 'png', + y: 16663, + }, + ], + }, + ], + hits: 171454, + xAxisOrderedValues: ['css', 'html', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js new file mode 100644 index 0000000000000..c1044160c0e7a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js @@ -0,0 +1,2918 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + title: 'Top 2 geo.dest: CN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 42, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 42, + value: 42, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 31, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 30, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 25, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 22, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 22, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 21, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 21, + value: 21, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 19, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 19, + value: 19, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 18, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 11, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 10, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 9, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 9, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 8, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 8, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 6, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 4, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 3, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 2, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 1, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [0, -90], + [0, -45], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 1, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, -45], + [-90, -45], + [-90, 0], + [-135, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 42, + }, + }, + }, + { + label: 'Top 2 geo.dest: IN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 32, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 32, + value: 32, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 31, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 28, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 28, + value: 28, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 27, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 27, + value: 27, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 24, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 24, + value: 24, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 23, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 17, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 17, + value: 17, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 16, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 16, + value: 16, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 14, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 14, + value: 14, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 13, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 13, + value: 13, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 9, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 6, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 6, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 5, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 4, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 3, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 1, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 1, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 1, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -90], + [180, -90], + [180, -45], + [135, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 1, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 32, + }, + }, + }, + ], +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js new file mode 100644 index 0000000000000..a26dc9bd8b181 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js @@ -0,0 +1,1326 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + valueFormatter: _.identity, + geohashGridAgg: { vis: { params: {} } }, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 608, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 608, + value: 608, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [0, 45], + [45, 45], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 522, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 522, + value: 522, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 90], + [0, 135], + [45, 135], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 517, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 517, + value: 517, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [-45, -45], + [0, -45], + [0, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 446, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 446, + value: 446, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -90], + [0, -45], + [45, -45], + [45, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 426, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 426, + value: 426, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [45, 45], + [90, 45], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 413, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 413, + value: 413, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [0, 90], + [45, 90], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 362, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 362, + value: 362, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [-45, 45], + [0, 45], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 352, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 352, + value: 352, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -135], + [0, -90], + [45, -90], + [45, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 216, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 216, + value: 216, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [0, 0], + [45, 0], + [45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 183, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 183, + value: 183, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [45, 90], + [90, 90], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 158, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 158, + value: 158, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 135], + [-45, 180], + [0, 180], + [0, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 139, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 139, + value: 139, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 90], + [45, 135], + [90, 135], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 110, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 110, + value: 110, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -135], + [45, -90], + [90, -90], + [90, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 101, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 101, + value: 101, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 90], + [-45, 135], + [0, 135], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 101, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 101, + value: 101, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [-45, 0], + [0, 0], + [0, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 92, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 92, + value: 92, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -90], + [45, -45], + [90, -45], + [90, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 75, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 75, + value: 75, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -180], + [45, -135], + [90, -135], + [90, -180], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 64, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 64, + value: 64, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [45, 0], + [90, 0], + [90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 36, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 36, + value: 36, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 135], + [45, 180], + [90, 180], + [90, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 34, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 34, + value: 34, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 135], + [0, 180], + [45, 180], + [45, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 30, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-90, -45], + [-45, -45], + [-45, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 16, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 16, + value: 16, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [-45, 90], + [0, 90], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 10, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-90, 0], + [-45, 0], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 6, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 135], + [-90, 180], + [-45, 180], + [-45, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, -22.5], + }, + properties: { + value: 6, + geohash: '2', + center: [-157.5, -22.5], + aggConfigResult: { + $parent: { + key: '2', + value: '2', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -180], + [-45, -135], + [0, -135], + [0, -180], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -67.5], + }, + properties: { + value: 4, + geohash: 'h', + center: [22.5, -67.5], + aggConfigResult: { + $parent: { + key: 'h', + value: 'h', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-90, 45], + [-45, 45], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -67.5], + }, + properties: { + value: 2, + geohash: 'n', + center: [112.5, -67.5], + aggConfigResult: { + $parent: { + key: 'n', + value: 'n', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 90], + [-90, 135], + [-45, 135], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -67.5], + }, + properties: { + value: 2, + geohash: 'j', + center: [67.5, -67.5], + aggConfigResult: { + $parent: { + key: 'j', + value: 'j', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-90, 90], + [-45, 90], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -135], + [-45, -90], + [0, -90], + [0, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -67.5], + }, + properties: { + value: 1, + geohash: '1', + center: [-112.5, -67.5], + aggConfigResult: { + $parent: { + key: '1', + value: '1', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -135], + [-90, -90], + [-45, -90], + [-45, -135], + ], + }, + }, + ], + properties: { + min: 1, + max: 608, + zoom: 2, + center: [5, 15], + }, + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js new file mode 100644 index 0000000000000..ca4cb2a7feee1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js @@ -0,0 +1,2858 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + title: 'Top 2 geo.dest: CN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 39, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 39, + value: 39, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 31, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 30, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 25, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 23, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 23, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 22, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 20, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 20, + value: 20, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 18, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 11, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 11, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 10, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 10, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 8, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 8, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 6, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 4, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 3, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 2, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 1, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [0, -90], + [0, -45], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, -45], + [-90, -45], + [-90, 0], + [-135, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 39, + }, + }, + }, + { + label: 'Top 2 geo.dest: IN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 31, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 30, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 29, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 29, + value: 29, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 28, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 28, + value: 28, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 25, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 24, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 24, + value: 24, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 20, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 20, + value: 20, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 18, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 14, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 14, + value: 14, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 11, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 9, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 6, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 6, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 5, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 5, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 4, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 3, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 1, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 1, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 1, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -90], + [180, -90], + [180, -45], + [135, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 1, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 31, + }, + }, + }, + ], + hits: 1639, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js new file mode 100644 index 0000000000000..c93365234d158 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js @@ -0,0 +1,381 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + label: '404: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 1, + y0: 0, + }, + { + x: 3221225400, + y: 0, + y0: 0, + }, + { + x: 4294967200, + y: 0, + y0: 0, + }, + { + x: 5368709100, + y: 0, + y0: 0, + }, + { + x: 6442450900, + y: 0, + y0: 0, + }, + { + x: 7516192700, + y: 0, + y0: 0, + }, + { + x: 8589934500, + y: 0, + y0: 0, + }, + { + x: 10737418200, + y: 0, + y0: 0, + }, + { + x: 11811160000, + y: 0, + y0: 0, + }, + { + x: 12884901800, + y: 1, + y0: 0, + }, + { + x: 13958643700, + y: 0, + y0: 0, + }, + { + x: 15032385500, + y: 0, + y0: 0, + }, + { + x: 16106127300, + y: 0, + y0: 0, + }, + { + x: 18253611000, + y: 0, + y0: 0, + }, + { + x: 19327352800, + y: 0, + y0: 0, + }, + { + x: 20401094600, + y: 0, + y0: 0, + }, + { + x: 21474836400, + y: 0, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '200: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 0, + y0: 0, + }, + { + x: 3221225400, + y: 2, + y0: 0, + }, + { + x: 4294967200, + y: 3, + y0: 0, + }, + { + x: 5368709100, + y: 3, + y0: 0, + }, + { + x: 6442450900, + y: 1, + y0: 0, + }, + { + x: 7516192700, + y: 1, + y0: 0, + }, + { + x: 8589934500, + y: 4, + y0: 0, + }, + { + x: 10737418200, + y: 0, + y0: 0, + }, + { + x: 11811160000, + y: 1, + y0: 0, + }, + { + x: 12884901800, + y: 1, + y0: 0, + }, + { + x: 13958643700, + y: 1, + y0: 0, + }, + { + x: 15032385500, + y: 2, + y0: 0, + }, + { + x: 16106127300, + y: 3, + y0: 0, + }, + { + x: 18253611000, + y: 4, + y0: 0, + }, + { + x: 19327352800, + y: 5, + y0: 0, + }, + { + x: 20401094600, + y: 2, + y0: 0, + }, + { + x: 21474836400, + y: 2, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 0, + y0: 0, + }, + { + x: 3221225400, + y: 0, + y0: 0, + }, + { + x: 4294967200, + y: 0, + y0: 0, + }, + { + x: 5368709100, + y: 0, + y0: 0, + }, + { + x: 6442450900, + y: 0, + y0: 0, + }, + { + x: 7516192700, + y: 0, + y0: 0, + }, + { + x: 8589934500, + y: 0, + y0: 0, + }, + { + x: 10737418200, + y: 1, + y0: 0, + }, + { + x: 11811160000, + y: 0, + y0: 0, + }, + { + x: 12884901800, + y: 0, + y0: 0, + }, + { + x: 13958643700, + y: 0, + y0: 0, + }, + { + x: 15032385500, + y: 0, + y0: 0, + }, + { + x: 16106127300, + y: 0, + y0: 0, + }, + { + x: 18253611000, + y: 0, + y0: 0, + }, + { + x: 19327352800, + y: 0, + y0: 0, + }, + { + x: 20401094600, + y: 0, + y0: 0, + }, + { + x: 21474836400, + y: 0, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 2147483600, + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 8589934500, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + ], + hits: 40, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js new file mode 100644 index 0000000000000..d88197c3737e5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js @@ -0,0 +1,225 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + label: '404: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 1, + }, + { + x: 10737418200, + y: 1, + }, + { + x: 15032385500, + y: 2, + }, + { + x: 19327352800, + y: 1, + }, + { + x: 32212254700, + y: 1, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '200: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 3221225400, + y: 4, + }, + { + x: 4294967200, + y: 3, + }, + { + x: 5368709100, + y: 3, + }, + { + x: 6442450900, + y: 2, + }, + { + x: 7516192700, + y: 2, + }, + { + x: 8589934500, + y: 2, + }, + { + x: 9663676400, + y: 3, + }, + { + x: 11811160000, + y: 3, + }, + { + x: 12884901800, + y: 2, + }, + { + x: 13958643700, + y: 1, + }, + { + x: 15032385500, + y: 2, + }, + { + x: 16106127300, + y: 3, + }, + { + x: 17179869100, + y: 1, + }, + { + x: 18253611000, + y: 4, + }, + { + x: 19327352800, + y: 1, + }, + { + x: 20401094600, + y: 1, + }, + { + x: 21474836400, + y: 4, + }, + { + x: 32212254700, + y: 3, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 10737418200, + y: 1, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 2147483600, + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 8589934500, + 9663676400, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 17179869100, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + 32212254700, + ], + hits: 51, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js new file mode 100644 index 0000000000000..99511e693ff02 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 3221225400, + y: 5, + }, + { + x: 4294967200, + y: 2, + }, + { + x: 5368709100, + y: 5, + }, + { + x: 6442450900, + y: 4, + }, + { + x: 7516192700, + y: 1, + }, + { + x: 9663676400, + y: 9, + }, + { + x: 10737418200, + y: 5, + }, + { + x: 11811160000, + y: 5, + }, + { + x: 12884901800, + y: 2, + }, + { + x: 13958643700, + y: 3, + }, + { + x: 15032385500, + y: 3, + }, + { + x: 16106127300, + y: 3, + }, + { + x: 17179869100, + y: 1, + }, + { + x: 18253611000, + y: 6, + }, + { + x: 19327352800, + y: 3, + }, + { + x: 20401094600, + y: 3, + }, + { + x: 21474836400, + y: 7, + }, + { + x: 32212254700, + y: 4, + }, + ], + }, + ], + hits: 71, + xAxisOrderedValues: [ + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 9663676400, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 17179869100, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + 32212254700, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js new file mode 100644 index 0000000000000..c23a89b755b5b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js @@ -0,0 +1,328 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + slices: { + children: [ + { + name: 0, + size: 378611, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 1000, + size: 205997, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 2000, + size: 397189, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 3000, + size: 397195, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 4000, + size: 398429, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 5000, + size: 397843, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 6000, + size: 398140, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 7000, + size: 398076, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 8000, + size: 396746, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 9000, + size: 397418, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 10000, + size: 20222, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 11000, + size: 20173, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 12000, + size: 20026, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 13000, + size: 19986, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 14000, + size: 20091, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 15000, + size: 20052, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 16000, + size: 20349, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 17000, + size: 20290, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 18000, + size: 20399, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 19000, + size: 20133, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 20000, + size: 9, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + ], + }, + names: [ + 0, + 1000, + 2000, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 11000, + 12000, + 13000, + 14000, + 15000, + 16000, + 17000, + 18000, + 19000, + 20000, + ], + hits: 3967374, + tooltipFormatter: function(event) { + return event.point; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js new file mode 100644 index 0000000000000..df71f4efc58b5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: '', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '_all', + y: 274, + }, + ], + }, + ], + hits: 274, + xAxisOrderedValues: ['_all'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js new file mode 100644 index 0000000000000..b5b931383f732 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + label: 'apache: _type', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 13309, + }, + { + x: '1000.0-2000.0', + y: 7196, + }, + ], + }, + ], + }, + { + label: 'nginx: _type', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 3278, + }, + { + x: '1000.0-2000.0', + y: 1804, + }, + ], + }, + ], + }, + ], + hits: 171499, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js new file mode 100644 index 0000000000000..bc7e4c9f49625 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + label: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 6422, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 3446, + y0: 0, + }, + ], + }, + ], + }, + { + label: + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 5430, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 3010, + y0: 0, + }, + ], + }, + ], + }, + { + label: + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 4735, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 2542, + y0: 0, + }, + ], + }, + ], + }, + ], + hits: 171501, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js new file mode 100644 index 0000000000000..40c14beeb4f3e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 16576, + }, + { + x: '1000.0-2000.0', + y: 9005, + }, + ], + }, + ], + hits: 171500, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js new file mode 100644 index 0000000000000..bf4fcb7e9e526 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + label: 'http: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128237, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10258, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'info: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 108148, + }, + { + x: 'info', + y: 96242, + }, + { + x: 'security', + y: 25889, + }, + { + x: 'error', + y: 7673, + }, + { + x: 'warning', + y: 12842, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.slate.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 98056, + }, + { + x: 'info', + y: 87344, + }, + { + x: 'security', + y: 23577, + }, + { + x: 'error', + y: 7004, + }, + { + x: 'warning', + y: 11759, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'twitter.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 74154, + }, + { + x: 'info', + y: 65963, + }, + { + x: 'security', + y: 17832, + }, + { + x: 'error', + y: 5258, + }, + { + x: 'warning', + y: 8906, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.www.slate.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 62591, + }, + { + x: 'info', + y: 55822, + }, + { + x: 'security', + y: 15100, + }, + { + x: 'error', + y: 4564, + }, + { + x: 'warning', + y: 7498, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171446, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js new file mode 100644 index 0000000000000..5d737131dc998 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + label: 'h3: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'h5: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'http: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'success: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 120689, + }, + { + x: 'info', + y: 107621, + }, + { + x: 'security', + y: 28916, + }, + { + x: 'error', + y: 8590, + }, + { + x: 'warning', + y: 14548, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.slate.com: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 62292, + }, + { + x: 'info', + y: 55646, + }, + { + x: 'security', + y: 14823, + }, + { + x: 'error', + y: 4441, + }, + { + x: 'warning', + y: 7539, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171445, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js new file mode 100644 index 0000000000000..36df8e091ba89 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 143995, + }, + { + x: 'info', + y: 128233, + }, + { + x: 'security', + y: 34515, + }, + { + x: 'error', + y: 10256, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + hits: 171439, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js new file mode 100644 index 0000000000000..a914f20a7ffc6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js @@ -0,0 +1,1654 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1416850340336, + max: 1416852140336, + }, + yAxisLabel: 'Count of documents', + xAxisOrderedValues: [ + 1416850320000, + 1416850350000, + 1416850380000, + 1416850410000, + 1416850440000, + 1416850470000, + 1416850500000, + 1416850530000, + 1416850560000, + 1416850590000, + 1416850620000, + 1416850650000, + 1416850680000, + 1416850710000, + 1416850740000, + 1416850770000, + 1416850800000, + 1416850830000, + 1416850860000, + 1416850890000, + 1416850920000, + 1416850950000, + 1416850980000, + 1416851010000, + 1416851040000, + 1416851070000, + 1416851100000, + 1416851130000, + 1416851160000, + 1416851190000, + 1416851220000, + 1416851250000, + 1416851280000, + 1416851310000, + 1416851340000, + 1416851370000, + 1416851400000, + 1416851430000, + 1416851460000, + 1416851490000, + 1416851520000, + 1416851550000, + 1416851580000, + 1416851610000, + 1416851640000, + 1416851670000, + 1416851700000, + 1416851730000, + 1416851760000, + 1416851790000, + 1416851820000, + 1416851850000, + 1416851880000, + 1416851910000, + 1416851940000, + 1416851970000, + 1416852000000, + 1416852030000, + 1416852060000, + 1416852090000, + 1416852120000, + ], + series: [ + { + label: 'jpg', + values: [ + { + x: 1416850320000, + y: 110, + y0: 0, + }, + { + x: 1416850350000, + y: 24, + y0: 0, + }, + { + x: 1416850380000, + y: 34, + y0: 0, + }, + { + x: 1416850410000, + y: 21, + y0: 0, + }, + { + x: 1416850440000, + y: 32, + y0: 0, + }, + { + x: 1416850470000, + y: 24, + y0: 0, + }, + { + x: 1416850500000, + y: 16, + y0: 0, + }, + { + x: 1416850530000, + y: 27, + y0: 0, + }, + { + x: 1416850560000, + y: 24, + y0: 0, + }, + { + x: 1416850590000, + y: 38, + y0: 0, + }, + { + x: 1416850620000, + y: 33, + y0: 0, + }, + { + x: 1416850650000, + y: 33, + y0: 0, + }, + { + x: 1416850680000, + y: 31, + y0: 0, + }, + { + x: 1416850710000, + y: 24, + y0: 0, + }, + { + x: 1416850740000, + y: 24, + y0: 0, + }, + { + x: 1416850770000, + y: 38, + y0: 0, + }, + { + x: 1416850800000, + y: 34, + y0: 0, + }, + { + x: 1416850830000, + y: 30, + y0: 0, + }, + { + x: 1416850860000, + y: 38, + y0: 0, + }, + { + x: 1416850890000, + y: 19, + y0: 0, + }, + { + x: 1416850920000, + y: 23, + y0: 0, + }, + { + x: 1416850950000, + y: 33, + y0: 0, + }, + { + x: 1416850980000, + y: 28, + y0: 0, + }, + { + x: 1416851010000, + y: 24, + y0: 0, + }, + { + x: 1416851040000, + y: 22, + y0: 0, + }, + { + x: 1416851070000, + y: 28, + y0: 0, + }, + { + x: 1416851100000, + y: 27, + y0: 0, + }, + { + x: 1416851130000, + y: 32, + y0: 0, + }, + { + x: 1416851160000, + y: 32, + y0: 0, + }, + { + x: 1416851190000, + y: 30, + y0: 0, + }, + { + x: 1416851220000, + y: 32, + y0: 0, + }, + { + x: 1416851250000, + y: 36, + y0: 0, + }, + { + x: 1416851280000, + y: 32, + y0: 0, + }, + { + x: 1416851310000, + y: 29, + y0: 0, + }, + { + x: 1416851340000, + y: 22, + y0: 0, + }, + { + x: 1416851370000, + y: 29, + y0: 0, + }, + { + x: 1416851400000, + y: 33, + y0: 0, + }, + { + x: 1416851430000, + y: 28, + y0: 0, + }, + { + x: 1416851460000, + y: 39, + y0: 0, + }, + { + x: 1416851490000, + y: 28, + y0: 0, + }, + { + x: 1416851520000, + y: 28, + y0: 0, + }, + { + x: 1416851550000, + y: 28, + y0: 0, + }, + { + x: 1416851580000, + y: 30, + y0: 0, + }, + { + x: 1416851610000, + y: 29, + y0: 0, + }, + { + x: 1416851640000, + y: 30, + y0: 0, + }, + { + x: 1416851670000, + y: 23, + y0: 0, + }, + { + x: 1416851700000, + y: 23, + y0: 0, + }, + { + x: 1416851730000, + y: 27, + y0: 0, + }, + { + x: 1416851760000, + y: 21, + y0: 0, + }, + { + x: 1416851790000, + y: 24, + y0: 0, + }, + { + x: 1416851820000, + y: 26, + y0: 0, + }, + { + x: 1416851850000, + y: 26, + y0: 0, + }, + { + x: 1416851880000, + y: 21, + y0: 0, + }, + { + x: 1416851910000, + y: 33, + y0: 0, + }, + { + x: 1416851940000, + y: 23, + y0: 0, + }, + { + x: 1416851970000, + y: 46, + y0: 0, + }, + { + x: 1416852000000, + y: 27, + y0: 0, + }, + { + x: 1416852030000, + y: 20, + y0: 0, + }, + { + x: 1416852060000, + y: 34, + y0: 0, + }, + { + x: 1416852090000, + y: 15, + y0: 0, + }, + { + x: 1416852120000, + y: 18, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1416850320000, + y: 3, + y0: 11, + }, + { + x: 1416850350000, + y: 13, + y0: 24, + }, + { + x: 1416850380000, + y: 5, + y0: 34, + }, + { + x: 1416850410000, + y: 12, + y0: 21, + }, + { + x: 1416850440000, + y: 9, + y0: 32, + }, + { + x: 1416850470000, + y: 12, + y0: 24, + }, + { + x: 1416850500000, + y: 6, + y0: 16, + }, + { + x: 1416850530000, + y: 6, + y0: 27, + }, + { + x: 1416850560000, + y: 11, + y0: 24, + }, + { + x: 1416850590000, + y: 11, + y0: 38, + }, + { + x: 1416850620000, + y: 6, + y0: 33, + }, + { + x: 1416850650000, + y: 8, + y0: 33, + }, + { + x: 1416850680000, + y: 6, + y0: 31, + }, + { + x: 1416850710000, + y: 4, + y0: 24, + }, + { + x: 1416850740000, + y: 9, + y0: 24, + }, + { + x: 1416850770000, + y: 3, + y0: 38, + }, + { + x: 1416850800000, + y: 5, + y0: 34, + }, + { + x: 1416850830000, + y: 6, + y0: 30, + }, + { + x: 1416850860000, + y: 9, + y0: 38, + }, + { + x: 1416850890000, + y: 5, + y0: 19, + }, + { + x: 1416850920000, + y: 8, + y0: 23, + }, + { + x: 1416850950000, + y: 9, + y0: 33, + }, + { + x: 1416850980000, + y: 5, + y0: 28, + }, + { + x: 1416851010000, + y: 6, + y0: 24, + }, + { + x: 1416851040000, + y: 9, + y0: 22, + }, + { + x: 1416851070000, + y: 9, + y0: 28, + }, + { + x: 1416851100000, + y: 11, + y0: 27, + }, + { + x: 1416851130000, + y: 5, + y0: 32, + }, + { + x: 1416851160000, + y: 8, + y0: 32, + }, + { + x: 1416851190000, + y: 6, + y0: 30, + }, + { + x: 1416851220000, + y: 10, + y0: 32, + }, + { + x: 1416851250000, + y: 5, + y0: 36, + }, + { + x: 1416851280000, + y: 6, + y0: 32, + }, + { + x: 1416851310000, + y: 4, + y0: 29, + }, + { + x: 1416851340000, + y: 8, + y0: 22, + }, + { + x: 1416851370000, + y: 3, + y0: 29, + }, + { + x: 1416851400000, + y: 8, + y0: 33, + }, + { + x: 1416851430000, + y: 10, + y0: 28, + }, + { + x: 1416851460000, + y: 5, + y0: 39, + }, + { + x: 1416851490000, + y: 7, + y0: 28, + }, + { + x: 1416851520000, + y: 6, + y0: 28, + }, + { + x: 1416851550000, + y: 4, + y0: 28, + }, + { + x: 1416851580000, + y: 9, + y0: 30, + }, + { + x: 1416851610000, + y: 3, + y0: 29, + }, + { + x: 1416851640000, + y: 9, + y0: 30, + }, + { + x: 1416851670000, + y: 6, + y0: 23, + }, + { + x: 1416851700000, + y: 11, + y0: 23, + }, + { + x: 1416851730000, + y: 4, + y0: 27, + }, + { + x: 1416851760000, + y: 8, + y0: 21, + }, + { + x: 1416851790000, + y: 5, + y0: 24, + }, + { + x: 1416851820000, + y: 7, + y0: 26, + }, + { + x: 1416851850000, + y: 7, + y0: 26, + }, + { + x: 1416851880000, + y: 4, + y0: 21, + }, + { + x: 1416851910000, + y: 8, + y0: 33, + }, + { + x: 1416851940000, + y: 6, + y0: 23, + }, + { + x: 1416851970000, + y: 6, + y0: 46, + }, + { + x: 1416852000000, + y: 3, + y0: 27, + }, + { + x: 1416852030000, + y: 6, + y0: 20, + }, + { + x: 1416852060000, + y: 5, + y0: 34, + }, + { + x: 1416852090000, + y: 5, + y0: 15, + }, + { + x: 1416852120000, + y: 1, + y0: 18, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1416850320000, + y: 1, + y0: 14, + }, + { + x: 1416850350000, + y: 2, + y0: 37, + }, + { + x: 1416850380000, + y: 4, + y0: 39, + }, + { + x: 1416850410000, + y: 2, + y0: 33, + }, + { + x: 1416850440000, + y: 3, + y0: 41, + }, + { + x: 1416850470000, + y: 1, + y0: 36, + }, + { + x: 1416850500000, + y: 1, + y0: 22, + }, + { + x: 1416850530000, + y: 1, + y0: 33, + }, + { + x: 1416850560000, + y: 2, + y0: 35, + }, + { + x: 1416850590000, + y: 5, + y0: 49, + }, + { + x: 1416850620000, + y: 1, + y0: 39, + }, + { + x: 1416850650000, + y: 1, + y0: 41, + }, + { + x: 1416850680000, + y: 4, + y0: 37, + }, + { + x: 1416850710000, + y: 1, + y0: 28, + }, + { + x: 1416850740000, + y: 3, + y0: 33, + }, + { + x: 1416850770000, + y: 2, + y0: 41, + }, + { + x: 1416850800000, + y: 2, + y0: 39, + }, + { + x: 1416850830000, + y: 5, + y0: 36, + }, + { + x: 1416850860000, + y: 3, + y0: 47, + }, + { + x: 1416850890000, + y: 1, + y0: 24, + }, + { + x: 1416850920000, + y: 3, + y0: 31, + }, + { + x: 1416850950000, + y: 4, + y0: 42, + }, + { + x: 1416850980000, + y: 3, + y0: 33, + }, + { + x: 1416851010000, + y: 5, + y0: 30, + }, + { + x: 1416851040000, + y: 2, + y0: 31, + }, + { + x: 1416851070000, + y: 3, + y0: 37, + }, + { + x: 1416851100000, + y: 5, + y0: 38, + }, + { + x: 1416851130000, + y: 3, + y0: 37, + }, + { + x: 1416851160000, + y: 4, + y0: 40, + }, + { + x: 1416851190000, + y: 9, + y0: 36, + }, + { + x: 1416851220000, + y: 7, + y0: 42, + }, + { + x: 1416851250000, + y: 2, + y0: 41, + }, + { + x: 1416851280000, + y: 1, + y0: 38, + }, + { + x: 1416851310000, + y: 2, + y0: 33, + }, + { + x: 1416851340000, + y: 5, + y0: 30, + }, + { + x: 1416851370000, + y: 3, + y0: 32, + }, + { + x: 1416851400000, + y: 5, + y0: 41, + }, + { + x: 1416851430000, + y: 4, + y0: 38, + }, + { + x: 1416851460000, + y: 5, + y0: 44, + }, + { + x: 1416851490000, + y: 2, + y0: 35, + }, + { + x: 1416851520000, + y: 2, + y0: 34, + }, + { + x: 1416851550000, + y: 4, + y0: 32, + }, + { + x: 1416851580000, + y: 3, + y0: 39, + }, + { + x: 1416851610000, + y: 4, + y0: 32, + }, + { + x: 1416851640000, + y: 0, + y0: 39, + }, + { + x: 1416851670000, + y: 2, + y0: 29, + }, + { + x: 1416851700000, + y: 1, + y0: 34, + }, + { + x: 1416851730000, + y: 3, + y0: 31, + }, + { + x: 1416851760000, + y: 0, + y0: 29, + }, + { + x: 1416851790000, + y: 4, + y0: 29, + }, + { + x: 1416851820000, + y: 3, + y0: 33, + }, + { + x: 1416851850000, + y: 3, + y0: 33, + }, + { + x: 1416851880000, + y: 0, + y0: 25, + }, + { + x: 1416851910000, + y: 0, + y0: 41, + }, + { + x: 1416851940000, + y: 3, + y0: 29, + }, + { + x: 1416851970000, + y: 3, + y0: 52, + }, + { + x: 1416852000000, + y: 1, + y0: 30, + }, + { + x: 1416852030000, + y: 5, + y0: 26, + }, + { + x: 1416852060000, + y: 3, + y0: 39, + }, + { + x: 1416852090000, + y: 1, + y0: 20, + }, + { + x: 1416852120000, + y: 2, + y0: 19, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1416850320000, + y: 1, + y0: 15, + }, + { + x: 1416850350000, + y: 6, + y0: 39, + }, + { + x: 1416850380000, + y: 6, + y0: 43, + }, + { + x: 1416850410000, + y: 5, + y0: 35, + }, + { + x: 1416850440000, + y: 3, + y0: 44, + }, + { + x: 1416850470000, + y: 5, + y0: 37, + }, + { + x: 1416850500000, + y: 6, + y0: 23, + }, + { + x: 1416850530000, + y: 1, + y0: 34, + }, + { + x: 1416850560000, + y: 3, + y0: 37, + }, + { + x: 1416850590000, + y: 2, + y0: 54, + }, + { + x: 1416850620000, + y: 1, + y0: 40, + }, + { + x: 1416850650000, + y: 1, + y0: 42, + }, + { + x: 1416850680000, + y: 2, + y0: 41, + }, + { + x: 1416850710000, + y: 5, + y0: 29, + }, + { + x: 1416850740000, + y: 7, + y0: 36, + }, + { + x: 1416850770000, + y: 2, + y0: 43, + }, + { + x: 1416850800000, + y: 3, + y0: 41, + }, + { + x: 1416850830000, + y: 6, + y0: 41, + }, + { + x: 1416850860000, + y: 2, + y0: 50, + }, + { + x: 1416850890000, + y: 4, + y0: 25, + }, + { + x: 1416850920000, + y: 2, + y0: 34, + }, + { + x: 1416850950000, + y: 3, + y0: 46, + }, + { + x: 1416850980000, + y: 8, + y0: 36, + }, + { + x: 1416851010000, + y: 4, + y0: 35, + }, + { + x: 1416851040000, + y: 4, + y0: 33, + }, + { + x: 1416851070000, + y: 1, + y0: 40, + }, + { + x: 1416851100000, + y: 2, + y0: 43, + }, + { + x: 1416851130000, + y: 4, + y0: 40, + }, + { + x: 1416851160000, + y: 3, + y0: 44, + }, + { + x: 1416851190000, + y: 4, + y0: 45, + }, + { + x: 1416851220000, + y: 2, + y0: 49, + }, + { + x: 1416851250000, + y: 4, + y0: 43, + }, + { + x: 1416851280000, + y: 8, + y0: 39, + }, + { + x: 1416851310000, + y: 4, + y0: 35, + }, + { + x: 1416851340000, + y: 4, + y0: 35, + }, + { + x: 1416851370000, + y: 7, + y0: 35, + }, + { + x: 1416851400000, + y: 2, + y0: 46, + }, + { + x: 1416851430000, + y: 3, + y0: 42, + }, + { + x: 1416851460000, + y: 3, + y0: 49, + }, + { + x: 1416851490000, + y: 3, + y0: 37, + }, + { + x: 1416851520000, + y: 4, + y0: 36, + }, + { + x: 1416851550000, + y: 3, + y0: 36, + }, + { + x: 1416851580000, + y: 4, + y0: 42, + }, + { + x: 1416851610000, + y: 5, + y0: 36, + }, + { + x: 1416851640000, + y: 3, + y0: 39, + }, + { + x: 1416851670000, + y: 3, + y0: 31, + }, + { + x: 1416851700000, + y: 2, + y0: 35, + }, + { + x: 1416851730000, + y: 5, + y0: 34, + }, + { + x: 1416851760000, + y: 4, + y0: 29, + }, + { + x: 1416851790000, + y: 5, + y0: 33, + }, + { + x: 1416851820000, + y: 1, + y0: 36, + }, + { + x: 1416851850000, + y: 3, + y0: 36, + }, + { + x: 1416851880000, + y: 6, + y0: 25, + }, + { + x: 1416851910000, + y: 4, + y0: 41, + }, + { + x: 1416851940000, + y: 7, + y0: 32, + }, + { + x: 1416851970000, + y: 5, + y0: 55, + }, + { + x: 1416852000000, + y: 2, + y0: 31, + }, + { + x: 1416852030000, + y: 2, + y0: 31, + }, + { + x: 1416852060000, + y: 4, + y0: 42, + }, + { + x: 1416852090000, + y: 6, + y0: 21, + }, + { + x: 1416852120000, + y: 2, + y0: 21, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1416850320000, + y: 0, + y0: 16, + }, + { + x: 1416850350000, + y: 1, + y0: 45, + }, + { + x: 1416850380000, + y: 0, + y0: 49, + }, + { + x: 1416850410000, + y: 2, + y0: 40, + }, + { + x: 1416850440000, + y: 0, + y0: 47, + }, + { + x: 1416850470000, + y: 0, + y0: 42, + }, + { + x: 1416850500000, + y: 3, + y0: 29, + }, + { + x: 1416850530000, + y: 1, + y0: 35, + }, + { + x: 1416850560000, + y: 3, + y0: 40, + }, + { + x: 1416850590000, + y: 2, + y0: 56, + }, + { + x: 1416850620000, + y: 2, + y0: 41, + }, + { + x: 1416850650000, + y: 5, + y0: 43, + }, + { + x: 1416850680000, + y: 2, + y0: 43, + }, + { + x: 1416850710000, + y: 1, + y0: 34, + }, + { + x: 1416850740000, + y: 2, + y0: 43, + }, + { + x: 1416850770000, + y: 2, + y0: 45, + }, + { + x: 1416850800000, + y: 1, + y0: 44, + }, + { + x: 1416850830000, + y: 1, + y0: 47, + }, + { + x: 1416850860000, + y: 1, + y0: 52, + }, + { + x: 1416850890000, + y: 1, + y0: 29, + }, + { + x: 1416850920000, + y: 2, + y0: 36, + }, + { + x: 1416850950000, + y: 2, + y0: 49, + }, + { + x: 1416850980000, + y: 0, + y0: 44, + }, + { + x: 1416851010000, + y: 3, + y0: 39, + }, + { + x: 1416851040000, + y: 2, + y0: 37, + }, + { + x: 1416851070000, + y: 2, + y0: 41, + }, + { + x: 1416851100000, + y: 2, + y0: 45, + }, + { + x: 1416851130000, + y: 0, + y0: 44, + }, + { + x: 1416851160000, + y: 1, + y0: 47, + }, + { + x: 1416851190000, + y: 2, + y0: 49, + }, + { + x: 1416851220000, + y: 4, + y0: 51, + }, + { + x: 1416851250000, + y: 0, + y0: 47, + }, + { + x: 1416851280000, + y: 3, + y0: 47, + }, + { + x: 1416851310000, + y: 3, + y0: 39, + }, + { + x: 1416851340000, + y: 2, + y0: 39, + }, + { + x: 1416851370000, + y: 2, + y0: 42, + }, + { + x: 1416851400000, + y: 3, + y0: 48, + }, + { + x: 1416851430000, + y: 1, + y0: 45, + }, + { + x: 1416851460000, + y: 0, + y0: 52, + }, + { + x: 1416851490000, + y: 2, + y0: 40, + }, + { + x: 1416851520000, + y: 1, + y0: 40, + }, + { + x: 1416851550000, + y: 3, + y0: 39, + }, + { + x: 1416851580000, + y: 1, + y0: 46, + }, + { + x: 1416851610000, + y: 2, + y0: 41, + }, + { + x: 1416851640000, + y: 1, + y0: 42, + }, + { + x: 1416851670000, + y: 2, + y0: 34, + }, + { + x: 1416851700000, + y: 3, + y0: 37, + }, + { + x: 1416851730000, + y: 1, + y0: 39, + }, + { + x: 1416851760000, + y: 1, + y0: 33, + }, + { + x: 1416851790000, + y: 1, + y0: 38, + }, + { + x: 1416851820000, + y: 1, + y0: 37, + }, + { + x: 1416851850000, + y: 1, + y0: 39, + }, + { + x: 1416851880000, + y: 1, + y0: 31, + }, + { + x: 1416851910000, + y: 2, + y0: 45, + }, + { + x: 1416851940000, + y: 0, + y0: 39, + }, + { + x: 1416851970000, + y: 0, + y0: 60, + }, + { + x: 1416852000000, + y: 1, + y0: 33, + }, + { + x: 1416852030000, + y: 2, + y0: 33, + }, + { + x: 1416852060000, + y: 1, + y0: 46, + }, + { + x: 1416852090000, + y: 1, + y0: 27, + }, + { + x: 1416852120000, + y: 0, + y0: 23, + }, + ], + }, + ], + hits: 2595, + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js new file mode 100644 index 0000000000000..8891d9badb2be --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js @@ -0,0 +1,159 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + columns: [ + { + label: 'logstash: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110710, + }, + { + x: 'css', + y: 27376, + }, + { + x: 'png', + y: 16664, + }, + { + x: 'gif', + y: 11264, + }, + { + x: 'php', + y: 5448, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '2014.11.12: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110643, + }, + { + x: 'css', + y: 27350, + }, + { + x: 'png', + y: 16648, + }, + { + x: 'gif', + y: 11257, + }, + { + x: 'php', + y: 5440, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '2014.11.11: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 67, + }, + { + x: 'css', + y: 26, + }, + { + x: 'png', + y: 16, + }, + { + x: 'gif', + y: 7, + }, + { + x: 'php', + y: 8, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + hits: 171462, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js new file mode 100644 index 0000000000000..09a1c15989760 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + rows: [ + { + label: '0.0-1000.0: bytes', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 3378, + }, + { + x: 'css', + y: 762, + }, + { + x: 'png', + y: 527, + }, + { + x: 'gif', + y: 11258, + }, + { + x: 'php', + y: 653, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '1000.0-2000.0: bytes', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 6422, + }, + { + x: 'css', + y: 1591, + }, + { + x: 'png', + y: 430, + }, + { + x: 'gif', + y: 8, + }, + { + x: 'php', + y: 561, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + hits: 171458, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js new file mode 100644 index 0000000000000..c55bff5631e88 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'Top 5 extension', + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110710, + }, + { + x: 'css', + y: 27389, + }, + { + x: 'png', + y: 16661, + }, + { + x: 'gif', + y: 11269, + }, + { + x: 'php', + y: 5447, + }, + ], + }, + ], + hits: 171476, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js new file mode 100644 index 0000000000000..372325120ee8e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export default { + xAxisOrderedValues: ['_all'], + yAxisLabel: 'Count', + zAxisLabel: 'machine.os.raw: Descending', + yScale: null, + series: [ + { + label: 'ios', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 2820, + series: 'ios', + }, + ], + }, + { + label: 'win 7', + aggId: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 2319, + series: 'win 7', + }, + ], + }, + { + label: 'win 8', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 1835, + series: 'win 8', + }, + ], + }, + { + label: 'windows xp service pack 2 version 20123452', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 734, + series: 'win xp', + }, + ], + }, + { + label: 'osx', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 1352, + series: 'osx', + }, + ], + }, + ], + hits: 14005, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + yAxisFormatter: function(val) { + return val; + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mocks.js b/src/plugins/vis_type_vislib/public/fixtures/mocks.js new file mode 100644 index 0000000000000..60edf6c1ff05c --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mocks.js @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { setFormatService } from '../services'; + +setFormatService({ + deserialize: () => ({ + convert: v => v, + }), +}); + +export const getMockUiState = () => { + const map = new Map(); + + return (() => ({ + get: (...args) => map.get(...args), + set: (...args) => map.set(...args), + setSilent: (...args) => map.set(...args), + on: () => undefined, + }))(); +}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/gauge.ts rename to src/plugins/vis_type_vislib/public/gauge.ts index 5e0b2b8fbd36c..561c45d26fa7f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -19,17 +19,11 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, GaugeTypes } from './utils/collections'; -import { - ColorModes, - ColorSchemas, - ColorSchemaParams, - Labels, - Style, -} from '../../../../plugins/charts/public'; +import { ColorModes, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/goal.ts rename to src/plugins/vis_type_vislib/public/goal.ts index 0f70dca69728d..5f74698938a0b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -23,9 +23,9 @@ import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { ColorModes, ColorSchemas } from '../../../../plugins/charts/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { ColorModes, ColorSchemas } from '../../charts/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts rename to src/plugins/vis_type_vislib/public/heatmap.ts index 9feed60b984ba..ced7a38568ffd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -19,15 +19,15 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; import { createVislibVisController } from './vis_controller'; import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, ValueAxis } from './types'; import { VisTypeVislibDependencies } from './plugin'; -import { ColorSchemas, ColorSchemaParams } from '../../../../plugins/charts/public'; +import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/histogram.ts rename to src/plugins/vis_type_vislib/public/histogram.ts index 54ccf66f362ca..52242ad11e8f5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -38,7 +38,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'histogram', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts rename to src/plugins/vis_type_vislib/public/horizontal_bar.ts index 6f73271726660..a58c15f136431 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -19,12 +19,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -38,7 +36,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/_index.scss b/src/plugins/vis_type_vislib/public/index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/_index.scss rename to src/plugins/vis_type_vislib/public/index.scss diff --git a/src/plugins/vis_type_vislib/public/index.ts b/src/plugins/vis_type_vislib/public/index.ts new file mode 100644 index 0000000000000..665643a6763f6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../core/public'; +import { VisTypeVislibPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} + +export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/line.ts rename to src/plugins/vis_type_vislib/public/line.ts index 1f9a8d77398e6..a94fd3f3945ab 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -19,12 +19,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -39,7 +37,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/pie.ts rename to src/plugins/vis_type_vislib/public/pie.ts index 2774836baa381..a68bc5893406f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts b/src/plugins/vis_type_vislib/public/pie_fn.test.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts rename to src/plugins/vis_type_vislib/public/pie_fn.test.ts index 15c80e4719487..a8c03eba2b449 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.test.ts @@ -18,12 +18,11 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; -jest.mock('ui/new_platform'); jest.mock('./vislib/response_handler', () => ({ vislibSlicesResponseHandler: jest.fn().mockReturnValue({ hits: 1, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts rename to src/plugins/vis_type_vislib/public/pie_fn.ts index 452e0be0df3e4..52da0f7ac14ec 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,11 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts new file mode 100644 index 0000000000000..e19a2ec451f2b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './index.scss'; + +import { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; +import { createPieVisFn } from './pie_fn'; +import { + createHistogramVisTypeDefinition, + createLineVisTypeDefinition, + createPieVisTypeDefinition, + createAreaVisTypeDefinition, + createHeatmapVisTypeDefinition, + createHorizontalBarVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, +} from './vis_type_vislib_vis_types'; +import { ChartsPluginSetup } from '../../charts/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService, setDataActions } from './services'; + +export interface VisTypeVislibDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeVislibPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; + visTypeXy?: VisTypeXyPluginSetup; +} + +/** @internal */ +export interface VisTypeVislibPluginStartDependencies { + data: DataPublicPluginStart; +} + +type VisTypeVislibCoreSetup = CoreSetup; + +/** @internal */ +export class VisTypeVislibPlugin implements Plugin { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeVislibCoreSetup, + { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + uiSettings: core.uiSettings, + charts, + }; + const vislibTypes = [ + createHistogramVisTypeDefinition, + createLineVisTypeDefinition, + createPieVisTypeDefinition, + createAreaVisTypeDefinition, + createHeatmapVisTypeDefinition, + createHorizontalBarVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, + ]; + const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; + + // if visTypeXy plugin is disabled it's config will be undefined + if (!visTypeXy) { + const convertedTypes: any[] = []; + const convertedFns: any[] = []; + + // Register legacy vislib types that have been converted + convertedFns.forEach(expressions.registerFunction); + convertedTypes.forEach(vis => + visualizations.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + // Register non-converted types + vislibFns.forEach(expressions.registerFunction); + vislibTypes.forEach(vis => + visualizations.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { + setFormatService(data.fieldFormats); + setDataActions(data.actions); + } +} diff --git a/src/plugins/vis_type_vislib/public/services.ts b/src/plugins/vis_type_vislib/public/services.ts new file mode 100644 index 0000000000000..633fae9c7f2a6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/services.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getDataActions, setDataActions] = createGetterSetter< + DataPublicPluginStart['actions'] +>('vislib data.actions'); + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('vislib data.fieldFormats'); diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts new file mode 100644 index 0000000000000..83d0b49b1c551 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TimeMarker } from './vislib/visualizations/time_marker'; +import { + Positions, + ChartModes, + ChartTypes, + AxisModes, + AxisTypes, + InterpolationModes, + ScaleTypes, + ThresholdLineStyles, +} from './utils/collections'; +import { Labels, Style } from '../../charts/public'; + +export interface CommonVislibParams { + addTooltip: boolean; + legendPosition: Positions; +} + +export interface Scale { + boundsMargin?: number | ''; + defaultYExtents?: boolean; + max?: number | null; + min?: number | null; + mode?: AxisModes; + setYExtents?: boolean; + type: ScaleTypes; +} + +interface ThresholdLine { + show: boolean; + value: number | null; + width: number | null; + style: ThresholdLineStyles; + color: string; +} + +export interface Axis { + id: string; + labels: Labels; + position: Positions; + scale: Scale; + show: boolean; + style: Style; + title: { text: string }; + type: AxisTypes; +} + +export interface ValueAxis extends Axis { + name: string; +} + +export interface SeriesParam { + data: { label: string; id: string }; + drawLinesBetweenPoints: boolean; + interpolate: InterpolationModes; + lineWidth?: number; + mode: ChartModes; + show: boolean; + showCircles: boolean; + type: ChartTypes; + valueAxis: string; +} + +export interface BasicVislibParams extends CommonVislibParams { + addTimeMarker: boolean; + categoryAxes: Axis[]; + orderBucketsBySum?: boolean; + labels: Labels; + thresholdLine: ThresholdLine; + valueAxes: ValueAxis[]; + grid: { + categoryLines: boolean; + valueAxis?: string; + }; + seriesParams: SeriesParam[]; + times: TimeMarker[]; +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts b/src/plugins/vis_type_vislib/public/utils/collections.ts similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts rename to src/plugins/vis_type_vislib/public/utils/collections.ts index 2024c43dd1c8b..44df4864bfd68 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts +++ b/src/plugins/vis_type_vislib/public/utils/collections.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { $Values } from '@kbn/utility-types'; -import { colorSchemas, Rotates } from '../../../../../plugins/charts/public'; +import { colorSchemas, Rotates } from '../../../charts/public'; export const Positions = Object.freeze({ RIGHT: 'right' as 'right', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx b/src/plugins/vis_type_vislib/public/utils/common_config.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx rename to src/plugins/vis_type_vislib/public/utils/common_config.tsx diff --git a/src/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 ec091e5d29cfd..65acc08b58da0 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 854b70b04e58a..a4243c6d25c41 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,11 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts rename to src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/VISLIB.md b/src/plugins/vis_type_vislib/public/vislib/VISLIB.md similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/VISLIB.md rename to src/plugins/vis_type_vislib/public/vislib/VISLIB.md diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_variables.scss b/src/plugins/vis_type_vislib/public/vislib/_variables.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_variables.scss rename to src/plugins/vis_type_vislib/public/vislib/_variables.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss rename to src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/data_array.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/data_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/data_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/data_array.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/index.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/index.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/labels.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js new file mode 100644 index 0000000000000..838275d44d2a1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js @@ -0,0 +1,469 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +import { labels } from './labels'; +import { dataArray } from './data_array'; +import { uniqLabels } from './uniq_labels'; +import { flattenSeries as getSeries } from './flatten_series'; + +let seriesLabels; +let rowsLabels; +let seriesArr; +let rowsArr; + +const seriesData = { + label: '', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], +}; + +const rowsData = { + rows: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +const columnsData = { + columns: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +describe('Vislib Labels Module Test Suite', function() { + let uniqSeriesLabels; + describe('Labels (main)', function() { + beforeEach(() => { + seriesLabels = labels(seriesData); + rowsLabels = labels(rowsData); + seriesArr = Array.isArray(seriesLabels); + rowsArr = Array.isArray(rowsLabels); + uniqSeriesLabels = _.chain(rowsData.rows) + .pluck('series') + .flattenDeep() + .pluck('label') + .uniq() + .value(); + }); + + it('should be a function', function() { + expect(typeof labels).toBe('function'); + }); + + it('should return an array if input is data.series', function() { + expect(seriesArr).toBe(true); + }); + + it('should return an array if input is data.rows', function() { + expect(rowsArr).toBe(true); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + labels('string not object'); + }).toThrow(); + }); + + it('should return unique label values', function() { + expect(rowsLabels[0]).toEqual(uniqSeriesLabels[0]); + expect(rowsLabels[1]).toEqual(uniqSeriesLabels[1]); + expect(rowsLabels[2]).toEqual(uniqSeriesLabels[2]); + }); + }); + + describe('Data array', function() { + const childrenObject = { + children: [], + }; + const seriesObject = { + series: [], + }; + const rowsObject = { + rows: [], + }; + const columnsObject = { + columns: [], + }; + const string = 'string'; + const number = 23; + const boolean = false; + const emptyArray = []; + const nullValue = null; + let notAValue; + let testSeries; + let testRows; + + beforeEach(() => { + seriesLabels = dataArray(seriesData); + rowsLabels = dataArray(rowsData); + testSeries = Array.isArray(seriesLabels); + testRows = Array.isArray(rowsLabels); + }); + + it('should throw an error if the input is not an object', function() { + expect(function() { + dataArray(string); + }).toThrow(); + + expect(function() { + dataArray(number); + }).toThrow(); + + expect(function() { + dataArray(boolean); + }).toThrow(); + + expect(function() { + dataArray(emptyArray); + }).toThrow(); + + expect(function() { + dataArray(nullValue); + }).toThrow(); + + expect(function() { + dataArray(notAValue); + }).toThrow(); + }); + + it( + 'should throw an error if property series, rows, or ' + 'columns is not present', + function() { + expect(function() { + dataArray(childrenObject); + }).toThrow(); + } + ); + + it( + 'should not throw an error if object has property series, rows, or ' + 'columns', + function() { + expect(function() { + dataArray(seriesObject); + }).not.toThrow(); + + expect(function() { + dataArray(rowsObject); + }).not.toThrow(); + + expect(function() { + dataArray(columnsObject); + }).not.toThrow(); + } + ); + + it('should be a function', function() { + expect(typeof dataArray).toEqual('function'); + }); + + it('should return an array of objects if input is data.series', function() { + expect(testSeries).toEqual(true); + }); + + it('should return an array of objects if input is data.rows', function() { + expect(testRows).toEqual(true); + }); + + it('should return an array of same length as input data.series', function() { + expect(seriesLabels.length).toEqual(seriesData.series.length); + }); + + it('should return an array of same length as input data.rows', function() { + expect(rowsLabels.length).toEqual(rowsData.rows.length); + }); + + it('should return an array of objects with obj.labels and obj.values', function() { + expect(seriesLabels[0].label).toEqual('100'); + expect(seriesLabels[0].values[0].x).toEqual(0); + expect(seriesLabels[0].values[0].y).toEqual(1); + }); + }); + + describe('Unique labels', function() { + const arrObj = [ + { label: 'a' }, + { label: 'b' }, + { label: 'b' }, + { label: 'c' }, + { label: 'c' }, + { label: 'd' }, + { label: 'f' }, + ]; + const string = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const emptyObject = {}; + const emptyArray = []; + let notAValue; + let uniq; + let testArr; + + beforeEach(() => { + uniq = uniqLabels(arrObj, function(d) { + return d; + }); + testArr = Array.isArray(uniq); + }); + + it('should throw an error if input is not an array', function() { + expect(function() { + uniqLabels(string); + }).toThrow(); + + expect(function() { + uniqLabels(number); + }).toThrow(); + + expect(function() { + uniqLabels(boolean); + }).toThrow(); + + expect(function() { + uniqLabels(nullValue); + }).toThrow(); + + expect(function() { + uniqLabels(emptyObject); + }).toThrow(); + + expect(function() { + uniqLabels(notAValue); + }).toThrow(); + }); + + it('should not throw an error if the input is an array', function() { + expect(function() { + uniqLabels(emptyArray); + }).not.toThrow(); + }); + + it('should be a function', function() { + expect(typeof uniqLabels).toBe('function'); + }); + + it('should return an array', function() { + expect(testArr).toBe(true); + }); + + it('should return array of 5 unique values', function() { + expect(uniq.length).toBe(5); + }); + }); + + describe('Get series', function() { + const string = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const rowsObject = { + rows: [], + }; + const columnsObject = { + columns: [], + }; + const emptyObject = {}; + const emptyArray = []; + let notAValue; + let columnsLabels; + let rowsLabels; + let columnsArr; + let rowsArr; + + beforeEach(() => { + columnsLabels = getSeries(columnsData); + rowsLabels = getSeries(rowsData); + columnsArr = Array.isArray(columnsLabels); + rowsArr = Array.isArray(rowsLabels); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + getSeries(string); + }).toThrow(); + + expect(function() { + getSeries(number); + }).toThrow(); + + expect(function() { + getSeries(boolean); + }).toThrow(); + + expect(function() { + getSeries(nullValue); + }).toThrow(); + + expect(function() { + getSeries(emptyArray); + }).toThrow(); + + expect(function() { + getSeries(notAValue); + }).toThrow(); + }); + + it('should throw an if property rows or columns is not set on the object', function() { + expect(function() { + getSeries(emptyObject); + }).toThrow(); + }); + + it('should not throw an error if rows or columns set on object', function() { + expect(function() { + getSeries(rowsObject); + }).not.toThrow(); + + expect(function() { + getSeries(columnsObject); + }).not.toThrow(); + }); + + it('should be a function', function() { + expect(typeof getSeries).toBe('function'); + }); + + it('should return an array if input is data.columns', function() { + expect(columnsArr).toBe(true); + }); + + it('should return an array if input is data.rows', function() { + expect(rowsArr).toBe(true); + }); + + it('should return an array of the same length as as input data.columns', function() { + expect(columnsLabels.length).toBe(columnsData.columns.length); + }); + + it('should return an array of the same length as as input data.rows', function() { + expect(rowsLabels.length).toBe(rowsData.rows.length); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap b/src/plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap rename to src/plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/components/legend/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss rename to src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts b/src/plugins/vis_type_vislib/public/vislib/components/legend/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts rename to src/plugins/vis_type_vislib/public/vislib/components/legend/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx similarity index 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 6bf66c2bdd788..c203642f353c5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -31,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 d9eea83d40b48..0167a542c6372 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts @@ -25,7 +25,7 @@ import _ from 'lodash'; * * > Duplicated utilty method from vislib Data class to decouple `vislib_vis_legend` from `vislib` * - * @see src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js + * @see src/plugins/vis_type_vislib/public/vislib/lib/data.js * * @returns {Array} Array of unique names (strings) */ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js new file mode 100644 index 0000000000000..953c115cc0d02 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import $ from 'jquery'; + +import { pointSeriesTooltipFormatter } from './_pointseries_tooltip_formatter'; + +describe('tooltipFormatter', function() { + const tooltipFormatter = pointSeriesTooltipFormatter(); + + function cell($row, i) { + return $row + .eq(i) + .text() + .trim(); + } + + const baseEvent = { + data: { + xAxisLabel: 'inner', + xAxisFormatter: _.identity, + yAxisLabel: 'middle', + yAxisFormatter: _.identity, + zAxisLabel: 'top', + zAxisFormatter: _.identity, + series: [ + { + rawId: '1', + label: 'middle', + zLabel: 'top', + yAxisFormatter: _.identity, + zAxisFormatter: _.identity, + }, + ], + }, + datum: { + x: 3, + y: 2, + z: 1, + extraMetrics: [], + seriesId: '1', + }, + }; + + it('returns html based on the mouse event', function() { + const event = _.cloneDeep(baseEvent); + const $el = $(tooltipFormatter(event)); + const $rows = $el.find('tr'); + expect($rows.length).toBe(3); + + const $row1 = $rows.eq(0).find('td'); + expect(cell($row1, 0)).toBe('inner'); + expect(cell($row1, 1)).toBe('3'); + + const $row2 = $rows.eq(1).find('td'); + expect(cell($row2, 0)).toBe('middle'); + expect(cell($row2, 1)).toBe('2'); + + const $row3 = $rows.eq(2).find('td'); + expect(cell($row3, 0)).toBe('top'); + expect(cell($row3, 1)).toBe('1'); + }); + + it('renders correctly on missing extraMetrics in datum', function() { + const event = _.cloneDeep(baseEvent); + delete event.datum.extraMetrics; + const $el = $(tooltipFormatter(event)); + const $rows = $el.find('tr'); + expect($rows.length).toBe(3); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js new file mode 100644 index 0000000000000..c184d0e9fbcf7 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js @@ -0,0 +1,274 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import $ from 'jquery'; +import _ from 'lodash'; +import sinon from 'sinon'; + +import { positionTooltip } from './position_tooltip'; + +describe('Tooltip Positioning', function() { + const sandbox = sinon.createSandbox(); + const positions = ['north', 'south', 'east', 'west']; + const bounds = ['top', 'left', 'bottom', 'right', 'area']; + let $window; + let $chart; + let $tooltip; + let $sizer; + + function testEl(width, height, $children) { + const $el = $('
'); + + const size = { + width: _.random(width[0], width[1]), + height: _.random(height[0], height[1]), + }; + + $el + .css({ + width: size.width, + height: size.height, + visibility: 'hidden', + }) + .appendTo('body'); + + if ($children) { + $el.append($children); + } + + $el.testSize = size; + + return $el; + } + + beforeEach(function() { + $window = testEl( + [500, 1000], + [600, 800], + ($chart = testEl([600, 750], [350, 550], ($tooltip = testEl([50, 100], [35, 75])))) + ); + + $sizer = $tooltip.clone().appendTo($window); + }); + + afterEach(function() { + $window.remove(); + $window = $chart = $tooltip = $sizer = null; + positionTooltip.removeClone(); + sandbox.restore(); + }); + + function makeEvent(xPercent, yPercent) { + xPercent = xPercent || 0.5; + yPercent = yPercent || 0.5; + + const base = $chart.offset(); + + return { + clientX: base.left + $chart.testSize.width * xPercent, + clientY: base.top + $chart.testSize.height * yPercent, + }; + } + + describe('getTtSize()', function() { + it('should measure the outer-size of the tooltip using an un-obstructed clone', function() { + const w = sandbox.spy($.fn, 'outerWidth'); + const h = sandbox.spy($.fn, 'outerHeight'); + + positionTooltip.getTtSize($tooltip.html(), $sizer); + + [w, h].forEach(function(spy) { + expect(spy).toHaveProperty('callCount', 1); + const matchHtml = w.thisValues.filter(function($t) { + return !$t.is($tooltip) && $t.html() === $tooltip.html(); + }); + expect(matchHtml).toHaveLength(1); + }); + }); + }); + + describe('getBasePosition()', function() { + it('calculates the offset values for the four positions', function() { + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + const pos = positionTooltip.getBasePosition(size, makeEvent()); + + positions.forEach(function(p) { + expect(pos).toHaveProperty(p); + }); + + expect(pos.north).toBeLessThan(pos.south); + expect(pos.east).toBeGreaterThan(pos.west); + }); + }); + + describe('getBounds()', function() { + it('returns the offsets for the tlrb of the element', function() { + const cbounds = positionTooltip.getBounds($chart); + + bounds.forEach(function(b) { + expect(cbounds).toHaveProperty(b); + }); + + expect(cbounds.top).toBeLessThan(cbounds.bottom); + expect(cbounds.left).toBeLessThan(cbounds.right); + }); + }); + + describe('getOverflow()', function() { + it('determines how much the base placement overflows the containing bounds in each direction', function() { + // size the tooltip very small so it won't collide with the edges + $tooltip.css({ width: 15, height: 15 }); + $sizer.css({ width: 15, height: 15 }); + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + expect(size).toHaveProperty('width', 15); + expect(size).toHaveProperty('height', 15); + + // position the element based on a mouse that is in the middle of the chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); + + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + positions.forEach(function(p) { + expect(overflow).toHaveProperty(p); + + // all positions should be less than 0 because the tooltip is so much smaller than the chart + expect(overflow[p]).toBeLessThan(0); + }); + }); + + it('identifies an overflow with a positive value in that direction', function() { + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + + // position the element based on a mouse that is in the bottom right hand corner of the chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.99, 0.99)); + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + + positions.forEach(function(p) { + expect(overflow).toHaveProperty(p); + + if (p === 'south' || p === 'east') { + expect(overflow[p]).toBeGreaterThan(0); + } else { + expect(overflow[p]).toBeLessThan(0); + } + }); + }); + + it('identifies only right overflow when tooltip overflows both sides of inner container but outer contains tooltip', function() { + // Size $tooltip larger than chart + const largeWidth = $chart.width() + 10; + $tooltip.css({ width: largeWidth }); + $sizer.css({ width: largeWidth }); + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + expect(size).toHaveProperty('width', largeWidth); + + // $chart is flush with the $window on the left side + expect(positionTooltip.getBounds($chart).left).toBe(0); + + // Size $window large enough for tooltip on right side + $window.css({ width: $chart.width() * 3 }); + + // Position click event in center of $chart so $tooltip overflows both sides of chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); + + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + + // no overflow on left (east) + expect(overflow.east).toBeLessThan(0); + // overflow on right (west) + expect(overflow.west).toBeGreaterThan(0); + }); + }); + + describe('positionTooltip() integration', function() { + it('returns nothing if the $chart or $tooltip are not passed in', function() { + expect(positionTooltip() === void 0).toBe(true); + expect(positionTooltip(null, null, null) === void 0).toBe(true); + expect(positionTooltip(null, $(), $()) === void 0).toBe(true); + }); + + function check(xPercent, yPercent /*, prev, directions... */) { + const directions = _.drop(arguments, 2); + const event = makeEvent(xPercent, yPercent); + const placement = positionTooltip({ + $window: $window, + $chart: $chart, + $sizer: $sizer, + event: event, + $el: $tooltip, + prev: _.isObject(directions[0]) ? directions.shift() : null, + }); + + expect(placement).toHaveProperty('top'); + expect(placement).toHaveProperty('left'); + + directions.forEach(function(dir) { + switch (dir) { + case 'top': + expect(placement.top).toBeLessThan(event.clientY); + return; + case 'bottom': + expect(placement.top).toBeGreaterThan(event.clientY); + return; + case 'right': + expect(placement.left).toBeGreaterThan(event.clientX); + return; + case 'left': + expect(placement.left).toBeLessThan(event.clientX); + return; + } + }); + + return placement; + } + + describe('calculates placement of the tooltip properly', function() { + it('mouse is in the middle', function() { + check(0.5, 0.5, 'bottom', 'right'); + }); + + it('mouse is in the top left', function() { + check(0.1, 0.1, 'bottom', 'right'); + }); + + it('mouse is in the top right', function() { + check(0.99, 0.1, 'bottom', 'left'); + }); + + it('mouse is in the bottom right', function() { + check(0.99, 0.99, 'top', 'left'); + }); + + it('mouse is in the bottom left', function() { + check(0.1, 0.99, 'top', 'right'); + }); + }); + + describe('maintain the direction of the tooltip on reposition', function() { + it('mouse moves from the top right to the middle', function() { + const pos = check(0.99, 0.1, 'bottom', 'left'); + check(0.5, 0.5, pos, 'bottom', 'left'); + }); + + it('mouse moves from the bottom left to the middle', function() { + const pos = check(0.1, 0.99, 'top', 'right'); + check(0.5, 0.5, pos, 'top', 'right'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js new file mode 100644 index 0000000000000..df502b7cde3df --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js @@ -0,0 +1,509 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { injectZeros } from './inject_zeros'; +import { orderXValues } from './ordered_x_keys'; +import { getUniqKeys } from './uniq_keys'; +import { flattenData } from './flatten_data'; +import { createZeroFilledArray } from './zero_filled_array'; +import { zeroFillDataArray } from './zero_fill_data_array'; + +describe('Vislib Zero Injection Module Test Suite', function() { + const dateHistogramRowsObj = { + xAxisOrderedValues: [ + 1418410560000, + 1418410620000, + 1418410680000, + 1418410740000, + 1418410800000, + 1418410860000, + 1418410920000, + ], + series: [ + { + label: 'html', + values: [ + { x: 1418410560000, y: 2 }, + { x: 1418410620000, y: 4 }, + { x: 1418410680000, y: 1 }, + { x: 1418410740000, y: 5 }, + { x: 1418410800000, y: 2 }, + { x: 1418410860000, y: 3 }, + { x: 1418410920000, y: 2 }, + ], + }, + { + label: 'css', + values: [ + { x: 1418410560000, y: 1 }, + { x: 1418410620000, y: 3 }, + { x: 1418410680000, y: 1 }, + { x: 1418410740000, y: 4 }, + { x: 1418410800000, y: 2 }, + ], + }, + ], + }; + const dateHistogramRows = dateHistogramRowsObj.series; + + const seriesDataObj = { + xAxisOrderedValues: ['v1', 'v2', 'v3', 'v4', 'v5'], + series: [ + { + label: '200', + values: [ + { x: 'v1', y: 234 }, + { x: 'v2', y: 34 }, + { x: 'v3', y: 834 }, + { x: 'v4', y: 1234 }, + { x: 'v5', y: 4 }, + ], + }, + ], + }; + const seriesData = seriesDataObj.series; + + const multiSeriesDataObj = { + xAxisOrderedValues: ['1', '2', '3', '4', '5'], + series: [ + { + label: '200', + values: [ + { x: '1', y: 234 }, + { x: '2', y: 34 }, + { x: '3', y: 834 }, + { x: '4', y: 1234 }, + { x: '5', y: 4 }, + ], + }, + { + label: '404', + values: [ + { x: '1', y: 1234 }, + { x: '3', y: 234 }, + { x: '5', y: 34 }, + ], + }, + { + label: '503', + values: [{ x: '3', y: 834 }], + }, + ], + }; + const multiSeriesData = multiSeriesDataObj.series; + + const multiSeriesNumberedDataObj = { + xAxisOrderedValues: [1, 2, 3, 4, 5], + series: [ + { + label: '200', + values: [ + { x: 1, y: 234 }, + { x: 2, y: 34 }, + { x: 3, y: 834 }, + { x: 4, y: 1234 }, + { x: 5, y: 4 }, + ], + }, + { + label: '404', + values: [ + { x: 1, y: 1234 }, + { x: 3, y: 234 }, + { x: 5, y: 34 }, + ], + }, + { + label: '503', + values: [{ x: 3, y: 834 }], + }, + ], + }; + const multiSeriesNumberedData = multiSeriesNumberedDataObj.series; + + const emptyObject = {}; + const str = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const emptyArray = []; + let notAValue; + + describe('Zero Injection (main)', function() { + let sample1; + let sample2; + let sample3; + + beforeEach(() => { + sample1 = injectZeros(seriesData, seriesDataObj); + sample2 = injectZeros(multiSeriesData, multiSeriesDataObj); + sample3 = injectZeros(multiSeriesNumberedData, multiSeriesNumberedDataObj); + }); + + it('should be a function', function() { + expect(_.isFunction(injectZeros)).toBe(true); + }); + + it('should return an object with series[0].values', function() { + expect(_.isObject(sample1)).toBe(true); + expect(_.isObject(sample1[0].values)).toBe(true); + }); + + it('should return the same array of objects when the length of the series array is 1', function() { + expect(sample1[0].values[0].x).toBe(seriesData[0].values[0].x); + expect(sample1[0].values[1].x).toBe(seriesData[0].values[1].x); + expect(sample1[0].values[2].x).toBe(seriesData[0].values[2].x); + expect(sample1[0].values[3].x).toBe(seriesData[0].values[3].x); + expect(sample1[0].values[4].x).toBe(seriesData[0].values[4].x); + }); + + it('should inject zeros in the input array', function() { + expect(sample2[1].values[1].y).toBe(0); + expect(sample2[2].values[0].y).toBe(0); + expect(sample2[2].values[1].y).toBe(0); + expect(sample2[2].values[4].y).toBe(0); + expect(sample3[1].values[1].y).toBe(0); + expect(sample3[2].values[0].y).toBe(0); + expect(sample3[2].values[1].y).toBe(0); + expect(sample3[2].values[4].y).toBe(0); + }); + + it('should return values arrays with the same x values', function() { + expect(sample2[1].values[0].x).toBe(sample2[2].values[0].x); + expect(sample2[1].values[1].x).toBe(sample2[2].values[1].x); + expect(sample2[1].values[2].x).toBe(sample2[2].values[2].x); + expect(sample2[1].values[3].x).toBe(sample2[2].values[3].x); + expect(sample2[1].values[4].x).toBe(sample2[2].values[4].x); + }); + + it('should return values arrays of the same length', function() { + expect(sample2[0].values.length).toBe(sample2[1].values.length); + expect(sample2[0].values.length).toBe(sample2[2].values.length); + expect(sample2[1].values.length).toBe(sample2[2].values.length); + }); + }); + + describe('Order X Values', function() { + let results; + let numberedResults; + + beforeEach(() => { + results = orderXValues(multiSeriesDataObj); + numberedResults = orderXValues(multiSeriesNumberedDataObj); + }); + + it('should return a function', function() { + expect(_.isFunction(orderXValues)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of values ordered by their index by default', function() { + expect(results[0]).toBe('1'); + expect(results[1]).toBe('2'); + expect(results[2]).toBe('3'); + expect(results[3]).toBe('4'); + expect(results[4]).toBe('5'); + expect(numberedResults[0]).toBe(1); + expect(numberedResults[1]).toBe(2); + expect(numberedResults[2]).toBe(3); + expect(numberedResults[3]).toBe(4); + expect(numberedResults[4]).toBe(5); + }); + + it('should return an array of values that preserve the index from xAxisOrderedValues', function() { + const data = { + xAxisOrderedValues: ['1', '2', '3', '4', '5'], + series: [ + { + label: '200', + values: [ + { x: '2', y: 34 }, + { x: '4', y: 1234 }, + ], + }, + { + label: '404', + values: [ + { x: '1', y: 1234 }, + { x: '3', y: 234 }, + { x: '5', y: 34 }, + ], + }, + { + label: '503', + values: [{ x: '3', y: 834 }], + }, + ], + }; + const result = orderXValues(data); + expect(result).toEqual(['1', '2', '3', '4', '5']); + }); + + it('should return an array of values ordered by their sum when orderBucketsBySum is true', function() { + const orderBucketsBySum = true; + results = orderXValues(multiSeriesDataObj, orderBucketsBySum); + numberedResults = orderXValues(multiSeriesNumberedDataObj, orderBucketsBySum); + + expect(results[0]).toBe('3'); + expect(results[1]).toBe('1'); + expect(results[2]).toBe('4'); + expect(results[3]).toBe('5'); + expect(results[4]).toBe('2'); + expect(numberedResults[0]).toBe(3); + expect(numberedResults[1]).toBe(1); + expect(numberedResults[2]).toBe(4); + expect(numberedResults[3]).toBe(5); + expect(numberedResults[4]).toBe(2); + }); + }); + + describe('Unique Keys', function() { + let results; + + beforeEach(() => { + results = getUniqKeys(multiSeriesDataObj); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + getUniqKeys(str); + }).toThrow(); + + expect(function() { + getUniqKeys(number); + }).toThrow(); + + expect(function() { + getUniqKeys(boolean); + }).toThrow(); + + expect(function() { + getUniqKeys(nullValue); + }).toThrow(); + + expect(function() { + getUniqKeys(emptyArray); + }).toThrow(); + + expect(function() { + getUniqKeys(notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(getUniqKeys)).toBe(true); + }); + + it('should return an object', function() { + expect(_.isObject(results)).toBe(true); + }); + + it('should return an object of unique keys', function() { + expect(_.uniq(_.keys(results)).length).toBe(_.keys(results).length); + }); + }); + + describe('Flatten Data', function() { + let results; + + beforeEach(() => { + results = flattenData(multiSeriesDataObj); + }); + + it('should return a function', function() { + expect(_.isFunction(flattenData)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results[0])).toBe(true); + expect(_.isObject(results[1])).toBe(true); + expect(_.isObject(results[2])).toBe(true); + }); + }); + + describe('Zero Filled Array', function() { + const arr1 = [1, 2, 3, 4, 5]; + const arr2 = ['1', '2', '3', '4', '5']; + let results1; + let results2; + + beforeEach(() => { + results1 = createZeroFilledArray(arr1); + results2 = createZeroFilledArray(arr2); + }); + + it('should throw an error if input is not an array', function() { + expect(function() { + createZeroFilledArray(str); + }).toThrow(); + + expect(function() { + createZeroFilledArray(number); + }).toThrow(); + + expect(function() { + createZeroFilledArray(boolean); + }).toThrow(); + + expect(function() { + createZeroFilledArray(nullValue); + }).toThrow(); + + expect(function() { + createZeroFilledArray(emptyObject); + }).toThrow(); + + expect(function() { + createZeroFilledArray(notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(createZeroFilledArray)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results1)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results1[0])).toBe(true); + expect(_.isObject(results1[1])).toBe(true); + expect(_.isObject(results1[2])).toBe(true); + expect(_.isObject(results1[3])).toBe(true); + expect(_.isObject(results1[4])).toBe(true); + }); + + it('should return an array of objects where each y value is 0', function() { + expect(results1[0].y).toBe(0); + expect(results1[1].y).toBe(0); + expect(results1[2].y).toBe(0); + expect(results1[3].y).toBe(0); + expect(results1[4].y).toBe(0); + }); + + it('should return an array of objects where each x values are numbers', function() { + expect(_.isNumber(results1[0].x)).toBe(true); + expect(_.isNumber(results1[1].x)).toBe(true); + expect(_.isNumber(results1[2].x)).toBe(true); + expect(_.isNumber(results1[3].x)).toBe(true); + expect(_.isNumber(results1[4].x)).toBe(true); + }); + + it('should return an array of objects where each x values are strings', function() { + expect(_.isString(results2[0].x)).toBe(true); + expect(_.isString(results2[1].x)).toBe(true); + expect(_.isString(results2[2].x)).toBe(true); + expect(_.isString(results2[3].x)).toBe(true); + expect(_.isString(results2[4].x)).toBe(true); + }); + }); + + describe('Zero Filled Data Array', function() { + const xValueArr = [1, 2, 3, 4, 5]; + let arr1; + const arr2 = [{ x: 3, y: 834 }]; + let results; + + beforeEach(() => { + arr1 = createZeroFilledArray(xValueArr); + // Takes zero array as 1st arg and data array as 2nd arg + results = zeroFillDataArray(arr1, arr2); + }); + + it('should throw an error if input are not arrays', function() { + expect(function() { + zeroFillDataArray(str, str); + }).toThrow(); + + expect(function() { + zeroFillDataArray(number, number); + }).toThrow(); + + expect(function() { + zeroFillDataArray(boolean, boolean); + }).toThrow(); + + expect(function() { + zeroFillDataArray(nullValue, nullValue); + }).toThrow(); + + expect(function() { + zeroFillDataArray(emptyObject, emptyObject); + }).toThrow(); + + expect(function() { + zeroFillDataArray(notAValue, notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(zeroFillDataArray)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results[0])).toBe(true); + expect(_.isObject(results[1])).toBe(true); + expect(_.isObject(results[2])).toBe(true); + }); + + it('should return an array with zeros injected in the appropriate objects as y values', function() { + expect(results[0].y).toBe(0); + expect(results[1].y).toBe(0); + expect(results[3].y).toBe(0); + expect(results[4].y).toBe(0); + }); + }); + + describe('Injected Zero values return in the correct order', function() { + let results; + + beforeEach(() => { + results = injectZeros(dateHistogramRows, dateHistogramRowsObj); + }); + + it('should return an array of objects', function() { + results.forEach(function(row) { + expect(Array.isArray(row.values)).toBe(true); + }); + }); + + it('should return ordered x values', function() { + const values = results[0].values; + expect(values[0].x).toBeLessThan(values[1].x); + expect(values[1].x).toBeLessThan(values[2].x); + expect(values[2].x).toBeLessThan(values[3].x); + expect(values[3].x).toBeLessThan(values[4].x); + expect(values[4].x).toBeLessThan(values[5].x); + expect(values[5].x).toBeLessThan(values[6].x); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts b/src/plugins/vis_type_vislib/public/vislib/errors.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts rename to src/plugins/vis_type_vislib/public/vislib/errors.ts index 9014349c38d25..c2965e8165759 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts +++ b/src/plugins/vis_type_vislib/public/vislib/errors.ts @@ -19,7 +19,7 @@ /* eslint-disable max-classes-per-file */ -import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { KbnError } from '../../../kibana_utils/public'; export class VislibError extends KbnError { constructor(message: string) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts index 2c6d62ed084b5..c3b82f72af482 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts @@ -18,7 +18,7 @@ */ import { toArray } from 'lodash'; -import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/common/types'; +import { SerializedFieldFormat } from '../../../../../expressions/common/types'; import { getFormatService } from '../../../services'; import { Table } from '../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts index 0c79c5b263cea..dc10c9f4938a0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatsRegistry } from '../../../../../../../plugins/data/common'; +import { IFieldFormatsRegistry } from '../../../../../data/common'; import { getPoint } from './_get_point'; import { setFormatService } from '../../../services'; import { Aspect } from './point_series'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/__snapshots__/dispatch_heatmap.test.js.snap b/src/plugins/vis_type_vislib/public/vislib/lib/__snapshots__/dispatch_heatmap.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/__snapshots__/dispatch_heatmap.test.js.snap rename to src/plugins/vis_type_vislib/public/vislib/lib/__snapshots__/dispatch_heatmap.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_alerts.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_alerts.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_alerts.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_alerts.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_data_label.js b/src/plugins/vis_type_vislib/public/vislib/lib/_data_label.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_data_label.js rename to src/plugins/vis_type_vislib/public/vislib/lib/_data_label.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_error_handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_error_handler.js rename to src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js new file mode 100644 index 0000000000000..b7764b92b7805 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ErrorHandler } from './_error_handler'; + +describe('Vislib ErrorHandler Test Suite', function() { + let errorHandler; + + beforeEach(() => { + errorHandler = new ErrorHandler(); + }); + + describe('validateWidthandHeight Method', function() { + it('should throw an error when width and/or height is 0', function() { + expect(function() { + errorHandler.validateWidthandHeight(0, 200); + }).toThrow(); + expect(function() { + errorHandler.validateWidthandHeight(200, 0); + }).toThrow(); + }); + + it('should throw an error when width and/or height is NaN', function() { + expect(function() { + errorHandler.validateWidthandHeight(null, 200); + }).toThrow(); + expect(function() { + errorHandler.validateWidthandHeight(200, null); + }).toThrow(); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_handler.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_handler.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/alerts.js b/src/plugins/vis_type_vislib/public/vislib/lib/alerts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/alerts.js rename to src/plugins/vis_type_vislib/public/vislib/lib/alerts.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js new file mode 100644 index 0000000000000..dec7de5ceeda9 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js @@ -0,0 +1,241 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib Axis Class Test Suite', function() { + let mockUiState; + let yAxis; + let el; + let fixture; + let seriesData; + + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734130000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + ], + }, + { + label: 'Count2', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734140000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + ], + }, + ], + xAxisFormatter: function(thing) { + return new Date(thing); + }, + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); + + fixture = el.append('div').attr('class', 'x-axis-div'); + + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + yAxis = new Axis(visConfig, { + type: 'value', + id: 'ValueAxis-1', + }); + + seriesData = data.series.map(series => { + return series.values; + }); + }); + + afterEach(function() { + fixture.remove(); + el.remove(); + }); + + describe('_stackNegAndPosVals Method', function() { + it('should correctly stack positive values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8, + }, + { + x: 1408734090000, + y: 23, + y0: 23, + }, + { + x: 1408734120000, + y: 30, + y0: 30, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 28, + }, + ]; + const stackedData = yAxis._stackNegAndPosVals(seriesData); + expect(stackedData[1]).toEqual(expectedResult); + }); + + it('should correctly stack pos and neg values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 0, + }, + { + x: 1408734090000, + y: 23, + y0: 0, + }, + { + x: 1408734120000, + y: 30, + y0: 0, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 0, + }, + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach(value => { + value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).toEqual(expectedResult); + }); + + it('should correctly stack mixed pos and neg values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8, + }, + { + x: 1408734090000, + y: 23, + y0: 0, + }, + { + x: 1408734120000, + y: 30, + y0: 30, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 28, + }, + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach((value, i) => { + if (i % 2 === 1) value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).toEqual(expectedResult); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js new file mode 100644 index 0000000000000..7901919d306d2 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js @@ -0,0 +1,211 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { AxisTitle } from './axis_title'; +import { AxisConfig } from './axis_config'; +import { VisConfig } from '../vis_config'; +import { Data } from '../data'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib AxisTitle Class Test Suite', function() { + let el; + let dataObj; + let xTitle; + let yTitle; + let visConfig; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper'); + + el.append('div') + .attr('class', 'visAxis__column--bottom') + .append('div') + .attr('class', 'axis-title y-axis-title') + .style('height', '20px') + .style('width', '20px'); + + el.append('div') + .attr('class', 'visAxis__column--left') + .append('div') + .attr('class', 'axis-title x-axis-title') + .style('height', '20px') + .style('width', '20px'); + + const uiState = getMockUiState(); + uiState.set('vis.colors', []); + dataObj = new Data(data, getMockUiState(), () => undefined); + visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + getMockUiState(), + el.node(), + () => undefined + ); + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + title: { + text: dataObj.get('xAxisLabel'), + }, + }); + const yAxisConfig = new AxisConfig(visConfig, { + position: 'left', + title: { + text: dataObj.get('yAxisLabel'), + }, + }); + xTitle = new AxisTitle(xAxisConfig); + yTitle = new AxisTitle(yAxisConfig); + }); + + afterEach(function() { + el.remove(); + }); + + it('should not do anything if title.show is set to false', function() { + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + show: false, + title: { + text: dataObj.get('xAxisLabel'), + }, + }); + xTitle = new AxisTitle(xAxisConfig); + xTitle.render(); + expect( + $(el.node()) + .find('.x-axis-title') + .find('svg').length + ).toBe(0); + }); + + describe('render Method', function() { + beforeEach(function() { + xTitle.render(); + yTitle.render(); + }); + + it('should append an svg to div', function() { + expect(el.select('.x-axis-title').selectAll('svg').length).toBe(1); + expect(el.select('.y-axis-title').selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect( + el + .select('.x-axis-title') + .selectAll('svg') + .select('g').length + ).toBe(1); + expect( + el + .select('.y-axis-title') + .selectAll('svg') + .select('g').length + ).toBe(1); + }); + + it('should append text', function() { + expect( + !!el + .select('.x-axis-title') + .selectAll('svg') + .selectAll('text') + ).toBe(true); + expect( + !!el + .select('.y-axis-title') + .selectAll('svg') + .selectAll('text') + ).toBe(true); + }); + }); + + describe('draw Method', function() { + it('should be a function', function() { + expect(_.isFunction(xTitle.draw())).toBe(true); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js new file mode 100644 index 0000000000000..d007a8a14de13 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js @@ -0,0 +1,254 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib xAxis Class Test Suite', function() { + let mockUiState; + let xAxis; + let el; + let fixture; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + xAxisOrderedValues: [ + 1408734060000, + 1408734090000, + 1408734120000, + 1408734150000, + 1408734180000, + 1408734210000, + 1408734240000, + 1408734270000, + 1408734300000, + 1408734330000, + ], + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisFormatter: function(thing) { + return new Date(thing); + }, + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); + + fixture = el.append('div').attr('class', 'x-axis-div'); + + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + xAxis = new Axis(visConfig, { + type: 'category', + id: 'CategoryAxis-1', + }); + }); + + afterEach(function() { + fixture.remove(); + el.remove(); + }); + + describe('render Method', function() { + beforeEach(function() { + xAxis.render(); + }); + + it('should append an svg to div', function() { + expect(el.selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect(el.selectAll('svg').select('g').length).toBe(1); + }); + + it('should append ticks with text', function() { + expect(!!el.selectAll('svg').selectAll('.tick text')).toBe(true); + }); + }); + + describe('getScale, getDomain, getTimeDomain, and getRange Methods', function() { + let timeScale; + let width; + let range; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + timeScale = xAxis.getScale(); + range = xAxis.axisScale.getRange(width); + }); + + it('should return a function', function() { + expect(_.isFunction(timeScale)).toBe(true); + }); + + it('should return the correct domain', function() { + expect(_.isDate(timeScale.domain()[0])).toBe(true); + expect(_.isDate(timeScale.domain()[1])).toBe(true); + }); + + it('should return the min and max dates', function() { + expect(timeScale.domain()[0].toDateString()).toBe(new Date(1408734060000).toDateString()); + expect(timeScale.domain()[1].toDateString()).toBe(new Date(1408734330000).toDateString()); + }); + + it('should return the correct range', function() { + expect(range[0]).toBe(0); + expect(range[1]).toBe(width); + }); + }); + + describe('getOrdinalDomain Method', function() { + let ordinalScale; + let ordinalDomain; + let width; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.ordered = null; + xAxis.axisConfig.ordered = null; + xAxis.getAxis(width); + ordinalScale = xAxis.getScale(); + ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); + }); + + it('should return an ordinal scale', function() { + expect(ordinalDomain.domain()[0]).toBe('this'); + expect(ordinalDomain.domain()[4]).toBe('array'); + }); + + it('should return an array of values', function() { + expect(Array.isArray(ordinalDomain.domain())).toBe(true); + }); + }); + + describe('getXScale Method', function() { + let width; + let xScale; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + xScale = xAxis.getScale(); + }); + + it('should return a function', function() { + expect(_.isFunction(xScale)).toBe(true); + }); + + it('should return a domain', function() { + expect(_.isDate(xScale.domain()[0])).toBe(true); + expect(_.isDate(xScale.domain()[1])).toBe(true); + }); + + it('should return a range', function() { + expect(xScale.range()[0]).toBe(0); + expect(xScale.range()[1]).toBe(width); + }); + }); + + describe('getXAxis Method', function() { + let width; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + }); + + it('should create an getScale function on the xAxis class', function() { + expect(_.isFunction(xAxis.getScale())).toBe(true); + }); + }); + + describe('draw Method', function() { + it('should be a function', function() { + expect(_.isFunction(xAxis.draw())).toBe(true); + }); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js new file mode 100644 index 0000000000000..85378ff1a14e8 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js @@ -0,0 +1,381 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import d3 from 'd3'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +const YAxis = Axis; +let mockUiState; +let el; +let buildYAxis; +let yAxis; +let yAxisDiv; + +const timeSeries = [ + 1408734060000, + 1408734090000, + 1408734120000, + 1408734150000, + 1408734180000, + 1408734210000, + 1408734240000, + 1408734270000, + 1408734300000, + 1408734330000, +]; + +const defaultGraphData = [ + [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], + [2, 13, 20, 18, 26, 20, 16, 12, 19, 14], +]; + +function makeSeriesData(data) { + return timeSeries.map(function(timestamp, i) { + return { + x: timestamp, + y: data[i] || 0, + }; + }); +} + +function createData(seriesData) { + const data = { + hits: 621, + label: 'test', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: seriesData.map(function(series) { + return { values: makeSeriesData(series) }; + }), + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + const node = $('
') + .css({ + height: 40, + width: 40, + }) + .appendTo('body') + .addClass('y-axis-wrapper') + .get(0); + + el = d3.select(node).datum(data); + + yAxisDiv = el.append('div').attr('class', 'y-axis-div'); + + buildYAxis = function(params) { + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + node, + () => undefined + ); + return new YAxis( + visConfig, + _.merge( + {}, + { + id: 'ValueAxis-1', + type: 'value', + scale: { + defaultYMin: true, + setYExtents: false, + }, + }, + params + ) + ); + }; + + yAxis = buildYAxis(); +} + +describe('Vislib yAxis Class Test Suite', function() { + beforeEach(() => { + mockUiState = getMockUiState(); + expect($('.y-axis-wrapper')).toHaveLength(0); + }); + + afterEach(function() { + if (el) { + el.remove(); + yAxisDiv.remove(); + } + }); + + describe('render Method', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.render(); + }); + + it('should append an svg to div', function() { + expect(el.selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect(el.selectAll('svg').select('g').length).toBe(1); + }); + + it('should append ticks with text', function() { + expect(!!el.selectAll('svg').selectAll('.tick text')).toBe(true); + }); + }); + + describe('getYScale Method', function() { + let yScale; + let graphData; + let domain; + const height = 50; + + function checkDomain(min, max) { + const domain = yScale.domain(); + expect(domain[0]).toBeLessThan(min + 1); + expect(domain[1]).toBeGreaterThan(max - 1); + return domain; + } + + function checkRange() { + expect(yScale.range()[0]).toBe(height); + expect(yScale.range()[1]).toBe(0); + } + + describe('API', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should return a function', function() { + expect(_.isFunction(yScale)).toBe(true); + }); + }); + + describe('positive values', function() { + beforeEach(function() { + graphData = defaultGraphData; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between 0 and max value', function() { + const min = 0; + const max = _.max(_.flattenDeep(graphData)); + const domain = checkDomain(min, max); + expect(domain[1]).toBeGreaterThan(0); + checkRange(); + }); + }); + + describe('negative values', function() { + beforeEach(function() { + graphData = [ + [-8, -23, -30, -28, -36, -30, -26, -22, -29, -24], + [-22, -8, -30, -4, 0, 0, -3, -22, -14, -24], + ]; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between min value and 0', function() { + const min = _.min(_.flattenDeep(graphData)); + const max = 0; + const domain = checkDomain(min, max); + expect(domain[0]).toBeLessThan(0); + checkRange(); + }); + }); + + describe('positive and negative values', function() { + beforeEach(function() { + graphData = [ + [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], + [22, 8, -30, -4, 0, 0, 3, -22, 14, 24], + ]; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between min and max values', function() { + const min = _.min(_.flattenDeep(graphData)); + const max = _.max(_.flattenDeep(graphData)); + const domain = checkDomain(min, max); + expect(domain[0]).toBeLessThan(0); + expect(domain[1]).toBeGreaterThan(0); + checkRange(); + }); + }); + + describe('validate user defined values', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.axisConfig.set('scale.stacked', true); + yAxis.axisConfig.set('scale.setYExtents', false); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should throw a NaN error', function() { + const min = 'Not a number'; + const max = 12; + + expect(function() { + yAxis.axisScale.validateUserExtents(min, max); + }).toThrow(); + }); + + it('should return a decimal value', function() { + yAxis.axisConfig.set('scale.mode', 'percentage'); + yAxis.axisConfig.set('scale.setYExtents', true); + yAxis.getAxis(height); + domain = []; + domain[0] = 20; + domain[1] = 80; + const newDomain = yAxis.axisScale.validateUserExtents(domain); + + expect(newDomain[0]).toBe(domain[0] / 100); + expect(newDomain[1]).toBe(domain[1] / 100); + }); + + it('should return the user defined value', function() { + domain = [20, 50]; + const newDomain = yAxis.axisScale.validateUserExtents(domain); + + expect(newDomain[0]).toBe(domain[0]); + expect(newDomain[1]).toBe(domain[1]); + }); + }); + + describe('should throw an error when', function() { + it('min === max', function() { + const min = 12; + const max = 12; + + expect(function() { + yAxis.axisScale.validateAxisExtents(min, max); + }).toThrow(); + }); + + it('min > max', function() { + const min = 30; + const max = 10; + + expect(function() { + yAxis.axisScale.validateAxisExtents(min, max); + }).toThrow(); + }); + }); + }); + + describe('getScaleType method', function() { + const fnNames = ['linear', 'log', 'square root']; + + it('should return a function', function() { + fnNames.forEach(function(fnName) { + expect(yAxis.axisScale.getD3Scale(fnName)).toEqual(expect.any(Function)); + }); + + // if no value is provided to the function, scale should default to a linear scale + expect(yAxis.axisScale.getD3Scale()).toEqual(expect.any(Function)); + }); + + it('should throw an error if function name is undefined', function() { + expect(function() { + yAxis.axisScale.getD3Scale('square'); + }).toThrow(); + }); + }); + + describe('_logDomain method', function() { + it('should throw an error', function() { + expect(function() { + yAxis.axisScale.logDomain(-10, -5); + }).toThrow(); + expect(function() { + yAxis.axisScale.logDomain(-10, 5); + }).toThrow(); + expect(function() { + yAxis.axisScale.logDomain(0, -5); + }).toThrow(); + }); + + it('should return a yMin value of 1', function() { + const yMin = yAxis.axisScale.logDomain(0, 200)[0]; + expect(yMin).toBe(1); + }); + }); + + describe('getYAxis method', function() { + let yMax; + beforeEach(function() { + createData(defaultGraphData); + yMax = yAxis.yMax; + }); + + afterEach(function() { + yAxis.yMax = yMax; + yAxis = buildYAxis(); + }); + + it('should use decimal format for small values', function() { + yAxis.yMax = 1; + const tickFormat = yAxis.getAxis().tickFormat(); + expect(tickFormat(0.8)).toBe('0.8'); + }); + }); + + describe('draw Method', function() { + beforeEach(function() { + createData(defaultGraphData); + }); + + it('should be a function', function() { + expect(_.isFunction(yAxis.draw())).toBe(true); + }); + }); + + describe('tickScale Method', function() { + beforeEach(function() { + createData(defaultGraphData); + }); + + it('should return the correct number of ticks', function() { + expect(yAxis.tickScale(1000)).toBe(11); + expect(yAxis.tickScale(40)).toBe(3); + expect(yAxis.tickScale(20)).toBe(0); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts b/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts rename to src/plugins/vis_type_vislib/public/vislib/lib/binder.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_grid.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_grid.js rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_title.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js b/src/plugins/vis_type_vislib/public/vislib/lib/data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js rename to src/plugins/vis_type_vislib/public/vislib/lib/data.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js new file mode 100644 index 0000000000000..b1a91979b3d9d --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js @@ -0,0 +1,309 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +import { Data } from './data'; +import { getMockUiState } from '../../fixtures/mocks'; + +const seriesData = { + label: '', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], +}; + +const rowsData = { + rows: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +const colsData = { + columns: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +describe('Vislib Data Class Test Suite', function() { + let mockUiState; + + beforeEach(() => { + mockUiState = getMockUiState(); + }); + + describe('Data Class (main)', function() { + it('should be a function', function() { + expect(_.isFunction(Data)).toBe(true); + }); + + it('should return an object', function() { + const rowIn = new Data(rowsData, mockUiState, () => undefined); + expect(_.isObject(rowIn)).toBe(true); + }); + }); + + describe('_removeZeroSlices', function() { + let data; + const pieData = { + slices: { + children: [{ size: 30 }, { size: 20 }, { size: 0 }], + }, + }; + + beforeEach(function() { + data = new Data(pieData, mockUiState, () => undefined); + }); + + it('should remove zero values', function() { + const slices = data._removeZeroSlices(data.data.slices); + expect(slices.children.length).toBe(2); + }); + }); + + describe('Data.flatten', function() { + let serIn; + let serOut; + + beforeEach(function() { + serIn = new Data(seriesData, mockUiState, () => undefined); + serOut = serIn.flatten(); + }); + + it('should return an array of value objects from every series', function() { + expect(serOut.every(_.isObject)).toBe(true); + }); + + it('should return all points from every series', testLength(seriesData)); + it('should return all points from every series in the rows', testLength(rowsData)); + it('should return all points from every series in the columns', testLength(colsData)); + + function testLength(inputData) { + return function() { + const data = new Data(inputData, mockUiState, () => undefined); + const len = _.reduce( + data.chartData(), + function(sum, chart) { + return ( + sum + + chart.series.reduce(function(sum, series) { + return sum + series.values.length; + }, 0) + ); + }, + 0 + ); + + expect(data.flatten()).toHaveLength(len); + }; + } + }); + + describe('geohashGrid methods', function() { + let data; + const geohashGridData = { + hits: 3954, + rows: [ + { + title: 'Top 5 _type: apache', + label: 'Top 5 _type: apache', + geoJson: { + type: 'FeatureCollection', + features: [], + properties: { + min: 2, + max: 331, + zoom: 3, + center: [47.517200697839414, -112.06054687499999], + }, + }, + }, + { + title: 'Top 5 _type: nginx', + label: 'Top 5 _type: nginx', + geoJson: { + type: 'FeatureCollection', + features: [], + properties: { + min: 1, + max: 88, + zoom: 3, + center: [47.517200697839414, -112.06054687499999], + }, + }, + }, + ], + }; + + beforeEach(function() { + data = new Data(geohashGridData, mockUiState, () => undefined); + }); + + describe('getVisData', function() { + it('should return the rows property', function() { + const visData = data.getVisData(); + expect(visData[0].title).toEqual(geohashGridData.rows[0].title); + }); + }); + + describe('getGeoExtents', function() { + it('should return the min and max geoJson properties', function() { + const minMax = data.getGeoExtents(); + expect(minMax.min).toBe(1); + expect(minMax.max).toBe(331); + }); + }); + }); + + describe('null value check', function() { + it('should return false', function() { + const data = new Data(rowsData, mockUiState, () => undefined); + expect(data.hasNullValues()).toBe(false); + }); + + it('should return true', function() { + const nullRowData = { rows: rowsData.rows.slice(0) }; + nullRowData.rows.push({ + label: 'e', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: null }, + { x: 2, y: 3 }, + ], + }, + ], + }); + + const data = new Data(nullRowData, mockUiState, () => undefined); + expect(data.hasNullValues()).toBe(true); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js similarity index 75% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js index e22f19ea643fd..4e650d4c20f97 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js @@ -17,12 +17,11 @@ * under the License. */ -import mockDispatchDataD3 from './fixtures/dispatch_heatmap_d3.json'; -import { Dispatch } from '../../lib/dispatch'; -import mockdataPoint from './fixtures/dispatch_heatmap_data_point.json'; -import mockConfigPercentage from './fixtures/dispatch_heatmap_config.json'; +import mockDispatchDataD3 from '../../fixtures/dispatch_heatmap_d3.json'; +import { Dispatch } from './dispatch'; +import mockdataPoint from '../../fixtures/dispatch_heatmap_data_point.json'; +import mockConfigPercentage from '../../fixtures/dispatch_heatmap_config.json'; -jest.mock('ui/new_platform'); jest.mock('d3', () => ({ event: { target: { @@ -32,15 +31,6 @@ jest.mock('d3', () => ({ }, }, })); -jest.mock('../../../legacy_imports.ts', () => ({ - ...jest.requireActual('../../../legacy_imports.ts'), - chrome: { - getUiSettingsClient: () => ({ - get: () => '', - }), - addBasePath: () => {}, - }, -})); function getHandlerMock(config = {}, data = {}) { return { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js new file mode 100644 index 0000000000000..a680788281fb1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import mockDispatchDataD3 from '../../fixtures/dispatch_bar_chart_d3.json'; +import { Dispatch } from './dispatch'; +import mockdataPoint from '../../fixtures/dispatch_bar_chart_data_point.json'; +import mockConfigPercentage from '../../fixtures/dispatch_bar_chart_config_percentage.json'; +import mockConfigNormal from '../../fixtures/dispatch_bar_chart_config_normal.json'; + +jest.mock('d3', () => ({ + event: { + target: { + nearestViewportElement: { + __data__: mockDispatchDataD3, + }, + }, + }, +})); + +function getHandlerMock(config = {}, data = {}) { + return { + visConfig: { get: (id, fallback) => config[id] || fallback }, + data, + }; +} + +describe('Vislib event responses dispatcher', () => { + test('return data for a vertical bars popover in percentage mode', () => { + const dataPoint = mockdataPoint; + const handlerMock = getHandlerMock(mockConfigPercentage); + const dispatch = new Dispatch(handlerMock); + const actual = dispatch.eventResponse(dataPoint, 0); + expect(actual.isPercentageMode).toBeTruthy(); + }); + + test('return data for a vertical bars popover in normal mode', () => { + const dataPoint = mockdataPoint; + const handlerMock = getHandlerMock(mockConfigNormal); + const dispatch = new Dispatch(handlerMock); + const actual = dispatch.eventResponse(dataPoint, 0); + expect(actual.isPercentageMode).toBeFalsy(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js new file mode 100644 index 0000000000000..f5b1c13f1a83f --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -0,0 +1,256 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import _ from 'lodash'; +import MarkdownIt from 'markdown-it'; + +import { NoResults } from '../errors'; +import { Layout } from './layout/layout'; +import { ChartTitle } from './chart_title'; +import { Alerts } from './alerts'; +import { Axis } from './axis/axis'; +import { ChartGrid as Grid } from './chart_grid'; +import { visTypes as chartTypes } from '../visualizations/vis_types'; +import { Binder } from './binder'; +import { dispatchRenderComplete } from '../../../../kibana_utils/public'; + +const markdownIt = new MarkdownIt({ + html: false, + linkify: true, +}); + +/** + * Handles building all the components of the visualization + * + * @class Handler + * @constructor + * @param vis {Object} Reference to the Vis Class Constructor + * @param opts {Object} Reference to Visualization constructors needed to + * create the visualization + */ +export class Handler { + constructor(vis, visConfig, deps) { + this.el = visConfig.get('el'); + this.ChartClass = chartTypes[visConfig.get('type')]; + this.deps = deps; + this.charts = []; + + this.vis = vis; + this.visConfig = visConfig; + this.data = visConfig.data; + + this.categoryAxes = visConfig + .get('categoryAxes') + .map(axisArgs => new Axis(visConfig, axisArgs)); + this.valueAxes = visConfig.get('valueAxes').map(axisArgs => new Axis(visConfig, axisArgs)); + this.chartTitle = new ChartTitle(visConfig); + this.alerts = new Alerts(this, visConfig.get('alerts')); + this.grid = new Grid(this, visConfig.get('grid')); + + if (visConfig.get('type') === 'point_series') { + this.data.stackData(this); + } + + if (visConfig.get('resize', false)) { + this.resize = visConfig.get('resize'); + } + + this.layout = new Layout(visConfig); + this.binder = new Binder(); + this.renderArray = _.filter([this.layout, this.chartTitle, this.alerts], Boolean); + + this.renderArray = this.renderArray + .concat(this.valueAxes) + // category axes need to render in reverse order https://github.com/elastic/kibana/issues/13551 + .concat(this.categoryAxes.slice().reverse()); + + // memoize so that the same function is returned every time, + // allowing us to remove/re-add the same function + this.getProxyHandler = _.memoize(function(eventType) { + const self = this; + return function(eventPayload) { + switch (eventType) { + case 'brush': + const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); + if (!xRaw) return; // not sure if this is possible? + return self.vis.emit(eventType, { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }); + case 'click': + return self.vis.emit(eventType, eventPayload); + } + }; + }); + + /** + * Enables events, i.e. binds specific events to the chart + * object(s) `on` method. For example, `click` or `mousedown` events. + * + * @method enable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.enable = this.chartEventProxyToggle('on'); + + /** + * Disables events for all charts + * + * @method disable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.disable = this.chartEventProxyToggle('off'); + } + /** + * Validates whether data is actually present in the data object + * used to render the Vis. Throws a no results error if data is not + * present. + * + * @private + */ + _validateData() { + const dataType = this.data.type; + + if (!dataType) { + throw new NoResults(); + } + } + + /** + * Renders the constructors that create the visualization, + * including the chart constructor + * + * @method render + * @returns {HTMLElement} With the visualization child element + */ + render() { + if (this.visConfig.get('error', null)) return this.error(this.visConfig.get('error')); + + const self = this; + const { binder, charts = [] } = this; + const selection = d3.select(this.el); + + selection.selectAll('*').remove(); + + this._validateData(); + this.renderArray.forEach(function(property) { + if (typeof property.render === 'function') { + property.render(); + } + }); + + // render the chart(s) + let loadedCount = 0; + const chartSelection = selection.selectAll('.chart'); + chartSelection.each(function(chartData) { + const chart = new self.ChartClass(self, this, chartData, self.deps); + + self.vis.eventNames().forEach(function(event) { + self.enable(event, chart); + }); + + binder.on(chart.events, 'rendered', () => { + loadedCount++; + if (loadedCount === chartSelection.length) { + // events from all charts are propagated to vis, we only need to fire renderComplete once they all finish + self.vis.emit('renderComplete'); + } + }); + + charts.push(chart); + chart.render(); + }); + } + + chartEventProxyToggle(method) { + return function(event, chart) { + const proxyHandler = this.getProxyHandler(event); + + _.each(chart ? [chart] : this.charts, function(chart) { + chart.events[method](event, proxyHandler); + }); + }; + } + + /** + * Removes all DOM elements from the HTML element provided + * + * @method removeAll + * @param el {HTMLElement} Reference to the HTML Element that + * contains the chart + * @returns {D3.Selection|D3.Transition.Transition} With the chart + * child element removed + */ + removeAll(el) { + return d3 + .select(el) + .selectAll('*') + .remove(); + } + + /** + * Displays an error message in the DOM + * + * @method error + * @param message {String} Error message to display + * @returns {HTMLElement} Displays the input message + */ + error(message) { + this.removeAll(this.el); + + const div = d3 + .select(this.el) + .append('div') + // class name needs `chart` in it for the polling checkSize function + // to continuously call render on resize + .attr('class', 'visError chart error') + .attr('data-test-subj', 'visLibVisualizeError'); + + div.append('h4').text(markdownIt.renderInline(message)); + + dispatchRenderComplete(this.el); + return div; + } + + /** + * Destroys all the charts in the visualization + * + * @method destroy + */ + destroy() { + this.binder.destroy(); + + this.renderArray.forEach(function(renderable) { + if (_.isFunction(renderable.destroy)) { + renderable.destroy(); + } + }); + + this.charts.splice(0).forEach(function(chart) { + if (_.isFunction(chart.destroy)) { + chart.destroy(); + } + }); + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js new file mode 100644 index 0000000000000..0bc11e5124a07 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +import { layoutTypes as layoutType } from './layout_types'; + +describe('Vislib Layout Types Test Suite', function() { + let layoutFunc; + + beforeEach(() => { + layoutFunc = layoutType.point_series; + }); + + it('should be an object', function() { + expect(_.isObject(layoutType)).toBe(true); + }); + + it('should return a function', function() { + expect(typeof layoutFunc).toBe('function'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js new file mode 100644 index 0000000000000..117b346efda89 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js @@ -0,0 +1,278 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import $ from 'jquery'; + +import { chartSplit } from './chart_split'; +import { chartTitleSplit } from './chart_title_split'; +import { xAxisSplit } from './x_axis_split'; +import { yAxisSplit } from './y_axis_split'; + +describe('Vislib Split Function Test Suite', function() { + describe('Column Chart', function() { + let el; + const data = { + rows: [ + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + ], + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); + + afterEach(function() { + el.remove(); + }); + + describe('chart split function', function() { + let fixture; + + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); + + afterEach(function() { + fixture.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart').length).toBe(2); + }); + + it('should add the correct class name', function() { + expect(!!$('.visWrapper__splitCharts--row').length).toBe(true); + }); + }); + + describe('chart title split function', function() { + let visEl; + let newEl; + let fixture; + + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + + newEl = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .datum({ series: [] }); + + newEl.append('div').attr('class', 'visAxis__splitTitles--x'); + newEl.append('div').attr('class', 'visAxis__splitTitles--y'); + newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + + fixture = newEl.selectAll(this.childNodes)[0].length; + }); + + afterEach(function() { + newEl.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart-title').length).toBe(2); + }); + + it('should remove the correct div', function() { + expect($('.visAxis__splitTitles--y').length).toBe(1); + expect($('.visAxis__splitTitles--x').length).toBe(0); + }); + + it('should remove all chart title divs when only one chart is rendered', function() { + expect(fixture).toBe(0); + }); + }); + + describe('x axis split function', function() { + let fixture; + let divs; + + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'columns') + .datum({ columns: [{}, {}] }); + d3.select('.columns').call(xAxisSplit); + divs = d3.selectAll('.x-axis-div')[0]; + }); + + afterEach(function() { + fixture.remove(); + $(divs).remove(); + }); + + it('should append the correct number of divs', function() { + expect(divs.length).toBe(2); + }); + }); + + describe('y axis split function', function() { + let fixture; + let divs; + + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'rows') + .datum({ rows: [{}, {}] }); + + d3.select('.rows').call(yAxisSplit); + + divs = d3.selectAll('.y-axis-div')[0]; + }); + + afterEach(function() { + fixture.remove(); + $(divs).remove(); + }); + + it('should append the correct number of divs', function() { + expect(divs.length).toBe(2); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js new file mode 100644 index 0000000000000..05f6f72246d4a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import $ from 'jquery'; + +import { chartSplit } from './chart_split'; +import { chartTitleSplit } from './chart_title_split'; + +describe('Vislib Gauge Split Function Test Suite', function() { + describe('Column Chart', function() { + let el; + const data = { + rows: [ + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + ], + }; + + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); + + afterEach(function() { + el.remove(); + }); + + describe('chart split function', function() { + let fixture; + + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); + + afterEach(function() { + fixture.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart').length).toBe(2); + }); + + it('should add the correct class name', function() { + expect(!!$('.visWrapper__splitCharts--row').length).toBe(true); + }); + }); + + describe('chart title split function', function() { + let visEl; + + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + }); + + afterEach(function() { + visEl.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.visAxis__splitTitles--x .chart-title').length).toBe(2); + expect($('.visAxis__splitTitles--y .chart-title').length).toBe(2); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js new file mode 100644 index 0000000000000..a27ee57e64a5a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import _ from 'lodash'; + +import { layoutTypes } from '../layout_types'; + +describe('Vislib Column Layout Test Suite', function() { + let columnLayout; + let el; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization'); + columnLayout = layoutTypes.point_series(el, data); + }); + + afterEach(function() { + el.remove(); + }); + + it('should return an array of objects', function() { + expect(Array.isArray(columnLayout)).toBe(true); + expect(_.isObject(columnLayout[0])).toBe(true); + }); + + it('should throw an error when the wrong number or no arguments provided', function() { + expect(function() { + layoutTypes.point_series(el); + }).toThrow(); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/gauge.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/gauge.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/gauge.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/pie.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/pie.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/pie.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/pie.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js new file mode 100644 index 0000000000000..684d8f346744e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js @@ -0,0 +1,232 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; +import { vislibPointSeriesTypes } from './point_series'; +import percentileTestdata from './testdata_linechart_percentile.json'; +import percentileTestdataResult from './testdata_linechart_percentile_result.json'; + +const maxBucketData = { + get: prop => { + return maxBucketData[prop] || maxBucketData.data[prop] || null; + }, + getLabels: () => [], + data: { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'series', + yAxisFormatter: () => 'test', + }, +}; + +describe('vislibPointSeriesTypes', () => { + const heatmapConfig = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + invertColors: false, + colorsRange: [], + heatmapMaxBuckets: 20, + }; + + const stackedData = { + get: prop => { + return stackedSeries[prop] || null; + }, + getLabels: () => [], + data: stackedSeries, + }; + + describe('axis formatters', () => { + it('should create a value axis config with the default y axis formatter', () => { + const parsedConfig = vislibPointSeriesTypes.line({}, maxBucketData); + expect(parsedConfig.valueAxes.length).toEqual(1); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe( + maxBucketData.data.yAxisFormatter + ); + }); + + it('should use the formatter of the first series matching the axis if there is a descriptor', () => { + const axisFormatter1 = jest.fn(); + const axisFormatter2 = jest.fn(); + const axisFormatter3 = jest.fn(); + const parsedConfig = vislibPointSeriesTypes.line( + { + valueAxes: [ + { + id: 'ValueAxis-1', + labels: {}, + }, + { + id: 'ValueAxis-2', + labels: {}, + }, + ], + seriesParams: [ + { + valueAxis: 'ValueAxis-1', + data: { + id: '2', + }, + }, + { + valueAxis: 'ValueAxis-2', + data: { + id: '3', + }, + }, + { + valueAxis: 'ValueAxis-2', + data: { + id: '4', + }, + }, + ], + }, + { + ...maxBucketData, + data: { + ...maxBucketData.data, + series: [ + { id: '2.1', label: 's1', values: [], yAxisFormatter: axisFormatter1 }, + { id: '2.2', label: 's2', values: [], yAxisFormatter: axisFormatter1 }, + { id: '3.1', label: 's3', values: [], yAxisFormatter: axisFormatter2 }, + { id: '3.2', label: 's4', values: [], yAxisFormatter: axisFormatter2 }, + { id: '4.1', label: 's5', values: [], yAxisFormatter: axisFormatter3 }, + { id: '4.2', label: 's6', values: [], yAxisFormatter: axisFormatter3 }, + ], + }, + } + ); + expect(parsedConfig.valueAxes.length).toEqual(2); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe(axisFormatter1); + expect(parsedConfig.valueAxes[1].labels.axisFormatter).toBe(axisFormatter2); + }); + }); + + describe('heatmap()', () => { + it('should return an error when more than 20 series are provided', () => { + const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, maxBucketData); + expect(parsedConfig.error).toMatchInlineSnapshot( + `"There are too many series defined (26). The configured maximum is 20."` + ); + }); + + it('should return valid config when less than 20 series are provided', () => { + const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, stackedData); + expect(parsedConfig.error).toBeUndefined(); + expect(parsedConfig.valueAxes[0].show).toBeFalsy(); + expect(parsedConfig.categoryAxes.length).toBe(2); + expect(parsedConfig.error).toBeUndefined(); + }); + }); +}); + +describe('Point Series Config Type Class Test Suite', function() { + let parsedConfig; + const histogramConfig = { + type: 'histogram', + addLegend: true, + tooltip: { + show: true, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + type: 'value', + labels: {}, + title: {}, + }, + ], + }; + + describe('histogram chart', function() { + beforeEach(function() { + parsedConfig = vislibPointSeriesTypes.column(histogramConfig, maxBucketData); + }); + it('should not throw an error when more than 25 series are provided', function() { + expect(parsedConfig.error).toBeUndefined(); + }); + + it('should set axis title and formatter from data', () => { + expect(parsedConfig.categoryAxes[0].title.text).toEqual(maxBucketData.data.xAxisLabel); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBeDefined(); + }); + }); + + describe('line chart', function() { + beforeEach(function() { + const percentileDataObj = { + get: prop => { + return maxBucketData[prop] || maxBucketData.data[prop] || null; + }, + getLabels: () => [], + data: percentileTestdata.data, + }; + parsedConfig = vislibPointSeriesTypes.line(percentileTestdata.cfg, percentileDataObj); + }); + it('should render a percentile line chart', function() { + expect(JSON.stringify(parsedConfig)).toEqual(JSON.stringify(percentileTestdataResult)); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile.json rename to src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile_result.json rename to src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/vis_config.js rename to src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js new file mode 100644 index 0000000000000..1ba7d4aaa8a0c --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js @@ -0,0 +1,140 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; + +import { VisConfig } from './vis_config'; +import { getMockUiState } from '../../fixtures/mocks'; + +describe('Vislib VisConfig Class Test Suite', function() { + let el; + let visConfig; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .node(); + + visConfig = new VisConfig( + { + type: 'point_series', + }, + data, + getMockUiState(), + el, + () => undefined + ); + }); + + afterEach(() => { + el.remove(); + }); + + describe('get Method', function() { + it('should be a function', function() { + expect(typeof visConfig.get).toBe('function'); + }); + + it('should get the property', function() { + expect(visConfig.get('el')).toBe(el); + expect(visConfig.get('type')).toBe('point_series'); + }); + + it('should return defaults if property does not exist', function() { + expect(visConfig.get('this.does.not.exist', 'defaults')).toBe('defaults'); + }); + + it('should throw an error if property does not exist and defaults were not provided', function() { + expect(function() { + visConfig.get('this.does.not.exist'); + }).toThrow(); + }); + }); + + describe('set Method', function() { + it('should be a function', function() { + expect(typeof visConfig.set).toBe('function'); + }); + + it('should set a property', function() { + visConfig.set('this.does.not.exist', 'it.does.now'); + expect(visConfig.get('this.does.not.exist')).toBe('it.does.now'); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html b/src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html rename to src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js b/src/plugins/vis_type_vislib/public/vislib/response_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js rename to src/plugins/vis_type_vislib/public/vislib/response_handler.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts b/src/plugins/vis_type_vislib/public/vislib/response_handler.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts rename to src/plugins/vis_type_vislib/public/vislib/response_handler.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts b/src/plugins/vis_type_vislib/public/vislib/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts rename to src/plugins/vis_type_vislib/public/vislib/types.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/vis.js rename to src/plugins/vis_type_vislib/public/vislib/vis.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js index 62de6d8413664..deafe010b6773 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js @@ -20,7 +20,7 @@ import d3 from 'd3'; import _ from 'lodash'; -import { getHeatmapColors } from '../../../../../../../plugins/charts/public'; +import { getHeatmapColors } from '../../../../../charts/public'; const arcAngles = { angleFactor: 0.75, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js new file mode 100644 index 0000000000000..6f497ae057d72 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js @@ -0,0 +1,326 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import moment from 'moment'; + +import { isColorDark } from '@elastic/eui'; + +import { PointSeries } from './_point_series'; +import { getHeatmapColors } from '../../../../../../plugins/charts/public'; + +const defaults = { + color: undefined, // todo + fillColor: undefined, // todo +}; +/** + * Line Chart Visualization + * + * @class HeatmapChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ +export class HeatmapChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { + super(handler, chartEl, chartData, seriesConfigArgs, deps); + + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + + this.handler.visConfig.set('legend', { + labels: this.getHeatmapLabels(this.handler.visConfig), + colors: this.getHeatmapColors(this.handler.visConfig), + }); + + const colors = this.handler.visConfig.get('legend.colors', null); + if (colors) { + this.handler.vis.uiState.setSilent('vis.defaultColors', null); + this.handler.vis.uiState.setSilent('vis.defaultColors', colors); + } + } + + getHeatmapLabels(cfg) { + const percentageMode = cfg.get('percentageMode'); + const colorsNumber = cfg.get('colorsNumber'); + const colorsRange = cfg.get('colorsRange'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const zScale = this.getValueAxis().getScale(); + const [min, max] = zScale.domain(); + const labels = []; + const maxColorCnt = 10; + if (cfg.get('setColorRange')) { + colorsRange.forEach(range => { + const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; + const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; + labels.push(`${from} - ${to}`); + }); + } else { + if (max === min) { + return [min.toString()]; + } + for (let i = 0; i < colorsNumber; i++) { + let label; + let val = i / colorsNumber; + let nextVal = (i + 1) / colorsNumber; + if (percentageMode) { + val = Math.ceil(val * 100); + nextVal = Math.ceil(nextVal * 100); + label = `${val}% - ${nextVal}%`; + } else { + val = val * (max - min) + min; + nextVal = nextVal * (max - min) + min; + if (max - min > maxColorCnt) { + const valInt = Math.ceil(val); + if (i === 0) { + val = valInt === val ? val : valInt - 1; + } else { + val = valInt; + } + nextVal = Math.ceil(nextVal); + } + if (isFinite(val)) val = zAxisFormatter(val); + if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); + label = `${val} - ${nextVal}`; + } + + labels.push(label); + } + } + + return labels; + } + + getHeatmapColors(cfg) { + const invertColors = cfg.get('invertColors'); + const colorSchema = cfg.get('colorSchema'); + const labels = this.getHeatmapLabels(cfg); + const colors = {}; + for (const i in labels) { + if (labels[i]) { + const val = invertColors ? 1 - i / labels.length : i / labels.length; + colors[labels[i]] = getHeatmapColors(val, colorSchema); + } + } + return colors; + } + + addSquares(svg, data) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.handler.categoryAxes[1].getScale(); + const zScale = this.getValueAxis().getScale(); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const colorsNumber = this.handler.visConfig.get('colorsNumber'); + const setColorRange = this.handler.visConfig.get('setColorRange'); + const colorsRange = this.handler.visConfig.get('colorsRange'); + const color = this.handler.data.getColorFunc(); + const labels = this.handler.visConfig.get('legend.labels'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const showLabels = zAxisConfig.get('labels.show'); + const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); + + const layer = svg.append('g').attr('class', 'series'); + + const squares = layer.selectAll('g.square').data(data.values); + + squares.exit().remove(); + + let barWidth; + if (this.getCategoryAxis().axisConfig.isTimeDomain()) { + const { min, interval } = this.handler.data.get('ordered'); + const start = min; + const end = moment(min) + .add(interval) + .valueOf(); + + barWidth = xScale(end) - xScale(start); + if (!isHorizontal) barWidth *= -1; + } + + function x(d) { + return xScale(d.x); + } + + function y(d) { + return yScale(d.series); + } + + const [min, max] = zScale.domain(); + function getColorBucket(d) { + let val = 0; + if (setColorRange && colorsRange.length) { + const bucket = _.find(colorsRange, range => { + return range.from <= d.y && range.to > d.y; + }); + return bucket ? colorsRange.indexOf(bucket) : -1; + } else { + if (isNaN(min) || isNaN(max)) { + val = colorsNumber - 1; + } else if (min === max) { + val = 0; + } else { + val = (d.y - min) / (max - min); /* get val from 0 - 1 */ + val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); + } + } + if (d.y == null) { + return -1; + } + return !isNaN(val) ? val : -1; + } + + function label(d) { + const colorBucket = getColorBucket(d); + // colorBucket id should always GTE 0 + if (colorBucket < 0) d.hide = true; + return labels[colorBucket]; + } + + function z(d) { + if (label(d) === '') return 'transparent'; + return color(label(d)); + } + + const squareWidth = barWidth || xScale.rangeBand(); + const squareHeight = yScale.rangeBand(); + + squares + .enter() + .append('g') + .attr('class', 'square'); + + squares + .append('rect') + .attr('x', x) + .attr('width', squareWidth) + .attr('y', y) + .attr('height', squareHeight) + .attr('data-label', label) + .attr('fill', z) + .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') + .style('display', d => { + return d.hide ? 'none' : 'initial'; + }); + + // todo: verify that longest label is not longer than the barwidth + // or barwidth is not smaller than textheight (and vice versa) + // + if (showLabels) { + const rotate = zAxisConfig.get('labels.rotate'); + const rotateRad = (rotate * Math.PI) / 180; + const cellPadding = 5; + const maxLength = + Math.min( + Math.abs(squareWidth / Math.cos(rotateRad)), + Math.abs(squareHeight / Math.sin(rotateRad)) + ) - cellPadding; + const maxHeight = + Math.min( + Math.abs(squareWidth / Math.sin(rotateRad)), + Math.abs(squareHeight / Math.cos(rotateRad)) + ) - cellPadding; + + let labelColor; + if (overwriteLabelColor) { + // If overwriteLabelColor is true, use the manual specified color + labelColor = zAxisConfig.get('labels.color'); + } else { + // Otherwise provide a function that will calculate a light or dark color + labelColor = d => { + const bgColor = z(d); + const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); + return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) + ? '#FFF' + : '#222'; + }; + } + + let hiddenLabels = false; + squares + .append('text') + .text(d => zAxisFormatter(d.y)) + .style('display', function(d) { + const textLength = this.getBBox().width; + const textHeight = this.getBBox().height; + const textTooLong = textLength > maxLength; + const textTooWide = textHeight > maxHeight; + if (!d.hide && (textTooLong || textTooWide)) { + hiddenLabels = true; + } + return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; + }) + .style('dominant-baseline', 'central') + .style('text-anchor', 'middle') + .style('fill', labelColor) + .attr('x', function(d) { + const center = x(d) + squareWidth / 2; + return center; + }) + .attr('y', function(d) { + const center = y(d) + squareHeight / 2; + return center; + }) + .attr('transform', function(d) { + const horizontalCenter = x(d) + squareWidth / 2; + const verticalCenter = y(d) + squareHeight / 2; + return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; + }); + if (hiddenLabels) { + this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); + } + } + + if (isTooltip) { + squares.call(tooltip.render()); + } + + return squares.selectAll('rect'); + } + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + const self = this; + + return function(selection) { + selection.each(function() { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const squares = self.addSquares(svg, self.chartData); + self.addCircleEvents(squares); + + self.events.emit('rendered', { + chart: self.chartData, + }); + + return svg; + }); + }; + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts rename to src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js new file mode 100644 index 0000000000000..058bdb5de8785 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js @@ -0,0 +1,140 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import $ from 'jquery'; + +import series from '../../fixtures/mock_data/date_histogram/_series'; +import terms from '../../fixtures/mock_data/terms/_columns'; +import { TimeMarker } from './time_marker'; + +describe('Vislib Time Marker Test Suite', function() { + const height = 50; + const color = '#ff0000'; + const opacity = 0.5; + const width = 3; + const customClass = 'custom-time-marker'; + const dateMathTimes = ['now-1m', 'now-5m', 'now-15m']; + const myTimes = dateMathTimes.map(function(dateMathString) { + return { + time: dateMathString, + class: customClass, + color: color, + opacity: opacity, + width: width, + }; + }); + const getExtent = function(dataArray, func) { + return func(dataArray, function(obj) { + return func(obj.values, function(d) { + return d.x; + }); + }); + }; + const times = []; + let defaultMarker; + let customMarker; + let selection; + let xScale; + let minDomain; + let maxDomain; + let domain; + + beforeEach(function() { + minDomain = getExtent(series.series, d3.min); + maxDomain = getExtent(series.series, d3.max); + domain = [minDomain, maxDomain]; + xScale = d3.time + .scale() + .domain(domain) + .range([0, 500]); + defaultMarker = new TimeMarker(times, xScale, height); + customMarker = new TimeMarker(myTimes, xScale, height); + + selection = d3 + .select('body') + .append('div') + .attr('class', 'marker'); + selection.datum(series); + }); + + afterEach(function() { + selection.remove('*'); + selection = null; + defaultMarker = null; + }); + + describe('_isTimeBaseChart method', function() { + let boolean; + let newSelection; + + it('should return true when data is time based', function() { + boolean = defaultMarker._isTimeBasedChart(selection); + expect(boolean).toBe(true); + }); + + it('should return false when data is not time based', function() { + newSelection = selection.datum(terms); + boolean = defaultMarker._isTimeBasedChart(newSelection); + expect(boolean).toBe(false); + }); + }); + + describe('render method', function() { + let lineArray; + + beforeEach(function() { + defaultMarker.render(selection); + customMarker.render(selection); + lineArray = document.getElementsByClassName('custom-time-marker'); + }); + + it('should render the default line', function() { + expect(!!$('line.time-marker').length).toBe(true); + }); + + it('should render the custom (user defined) lines', function() { + expect($('line.custom-time-marker').length).toBe(myTimes.length); + }); + + it('should set the class', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(line.getAttribute('class')).toBe(customClass); + }); + }); + + it('should set the stroke', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(line.getAttribute('stroke')).toBe(color); + }); + }); + + it('should set the stroke-opacity', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(+line.getAttribute('stroke-opacity')).toBe(opacity); + }); + }); + + it('should set the stroke-width', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(+line.getAttribute('stroke-width')).toBe(width); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js new file mode 100644 index 0000000000000..df044f46460c8 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +import { visTypes } from './vis_types'; + +describe('Vislib Vis Types Test Suite', function() { + let visFunc; + + beforeEach(function() { + visFunc = visTypes.point_series; + }); + + it('should be an object', function() { + expect(_.isObject(visTypes)).toBe(true); + }); + + it('should return a function', function() { + expect(typeof visFunc).toBe('function'); + }); +}); diff --git a/src/plugins/vis_type_vislib/server/index.ts b/src/plugins/vis_type_vislib/server/index.ts new file mode 100644 index 0000000000000..355c01d255ce7 --- /dev/null +++ b/src/plugins/vis_type_vislib/server/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/visualize/public/application/application.ts b/src/plugins/visualize/public/application/application.ts index 9d8a1b98ef023..19551bba9a43e 100644 --- a/src/plugins/visualize/public/application/application.ts +++ b/src/plugins/visualize/public/application/application.ts @@ -20,6 +20,8 @@ import './index.scss'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 1ec7c831b4864..f87edbf04f220 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -25,6 +25,7 @@ import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); +const buildHash = String(Number.MAX_SAFE_INTEGER); module.exports = function(grunt) { function pickBrowser() { @@ -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 c5bfc847d0041..0c4028905657d 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,5 +33,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js new file mode 100644 index 0000000000000..c79f5cb470890 --- /dev/null +++ b/test/api_integration/apis/telemetry/index.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ loadTestFile }) { + describe('Telemetry', () => { + loadTestFile(require.resolve('./telemetry_local')); + loadTestFile(require.resolve('./opt_in')); + loadTestFile(require.resolve('./telemetry_optin_notice_seen')); + }); +} diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts new file mode 100644 index 0000000000000..e4654ee3985f3 --- /dev/null +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('/api/telemetry/v2/optIn API', () => { + let defaultAttributes: TelemetrySavedObjectAttributes; + let kibanaVersion: any; + before(async () => { + const kibanaVersionAccessor = kibanaServer.version; + kibanaVersion = await kibanaVersionAccessor.get(); + defaultAttributes = + (await getSavedObjectAttributes(supertest).catch(err => { + if (err.message === 'expected 200 "OK", got 404 "Not Found"') { + return null; + } + throw err; + })) || {}; + + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + }); + + afterEach(async () => { + await updateSavedObjectAttributes(supertest, defaultAttributes); + }); + + it('should support sending false with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, false, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(false); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should support sending true with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, true, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(true); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should not support sending false with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, false, 400); + }); + + it('should not support sending true with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, true, 400); + }); + + it('should not support sending null', async () => { + await postTelemetryV2Optin(supertest, null, 400); + }); + + it('should not support sending junk', async () => { + await postTelemetryV2Optin(supertest, 42, 400); + }); + }); +} + +async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise { + const { body } = await supertest + .post('/api/telemetry/v2/optIn') + .set('kbn-xsrf', 'xxx') + .send({ enabled: value }) + .expect(statusCode); + + return body; +} + +async function updateSavedObjectAttributes( + supertest: any, + attributes: TelemetrySavedObjectAttributes +): Promise { + return await supertest + .post('/api/saved_objects/telemetry/telemetry') + .query({ overwrite: true }) + .set('kbn-xsrf', 'xxx') + .send({ attributes }) + .expect(200); +} + +async function getSavedObjectAttributes(supertest: any): Promise { + const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200); + return body.attributes; +} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js new file mode 100644 index 0000000000000..84bfd8a755c11 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; + +/* + * Create a single-level array with strings for all the paths to values in the + * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn + * in the tests. + */ +function flatKeys(source) { + const recursivelyFlatKeys = (obj, path = [], depth = 0) => { + return depth < 3 && _.isObject(obj) + ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1)) + : path.join('.'); + }; + + return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b)); +} + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/clusters/_stats', () => { + it('should pull local stats and validate data types', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.collection).to.be('local'); + expect(stats.stack_stats.kibana.count).to.be.a('number'); + expect(stats.stack_stats.kibana.indices).to.be.a('number'); + expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); + expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); + expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); + expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + }); + + it('should pull local stats and validate fields', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + const stats = body[0]; + + const actual = flatKeys(stats); + expect(actual).to.be.an('array'); + const expected = [ + 'cluster_name', + 'cluster_stats.cluster_uuid', + 'cluster_stats.indices.analysis', + 'cluster_stats.indices.completion', + 'cluster_stats.indices.count', + 'cluster_stats.indices.docs', + 'cluster_stats.indices.fielddata', + 'cluster_stats.indices.mappings', + 'cluster_stats.indices.query_cache', + 'cluster_stats.indices.segments', + 'cluster_stats.indices.shards', + 'cluster_stats.indices.store', + 'cluster_stats.nodes.count', + 'cluster_stats.nodes.discovery_types', + 'cluster_stats.nodes.fs', + 'cluster_stats.nodes.ingest', + 'cluster_stats.nodes.jvm', + 'cluster_stats.nodes.network_types', + 'cluster_stats.nodes.os', + 'cluster_stats.nodes.packaging_types', + 'cluster_stats.nodes.plugins', + 'cluster_stats.nodes.process', + 'cluster_stats.nodes.versions', + 'cluster_stats.status', + 'cluster_stats.timestamp', + 'cluster_uuid', + 'collection', + 'collectionSource', + 'stack_stats.kibana.count', + 'stack_stats.kibana.indices', + 'stack_stats.kibana.os', + 'stack_stats.kibana.plugins', + 'stack_stats.kibana.versions', + 'timestamp', + 'version', + ]; + + expect(expected.every(m => actual.includes(m))).to.be.ok(); + }); + }); +} diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts new file mode 100644 index 0000000000000..4413c672fb46c --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch'; +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const client: Client = getService('legacyEs'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + it('should update telemetry setting field via PUT', async () => { + try { + await client.delete({ + index: '.kibana', + id: 'telemetry:telemetry', + } as DeleteDocumentParams); + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + + await supertest + .put('/api/telemetry/v2/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { + _source: { telemetry }, + }: GetResponse<{ + telemetry: TelemetrySavedObjectAttributes; + }> = await client.get({ + index: '.kibana', + id: 'telemetry:telemetry', + } as GetParams); + + expect(telemetry.userHasSeenNotice).to.be(true); + }); + }); +} diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 862e5127bb670..93debdcc37f0a 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl: boolean; shouldLoginIfPrompted: boolean; useActualUrl: boolean; + insertTimestamp: boolean; } class CommonPage { @@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string) { + private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting @@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout ); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); currentUrl = await browser.getCurrentUrl(); log.debug(`Finished login process currentUrl = ${currentUrl}`); } @@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } private async navigate(navigateProps: NavigateProps) { - const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps; + const { + appConfig, + ensureCurrentUrl, + shouldLoginIfPrompted, + useActualUrl, + insertTimestamp, + } = navigateProps; const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); await retry.try(async () => { @@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = false, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = true, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (config.has(['apps', appName])) { @@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo log.debug('returned from get, calling refresh'); await browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts new file mode 100644 index 0000000000000..5ea151dffdc8e --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +function getCell(esaggsResult: any, column: number, row: number): unknown | undefined { + const columnId = esaggsResult?.columns[column]?.id; + if (!columnId) { + return; + } + return esaggsResult?.rows[row]?.[columnId]; +} + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('esaggs pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders tagcloud', () => { + it('filters on index pattern primary date field by default', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}' + `; + const result = await expectExpression('esaggs_primary_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(9375); + }); + + it('filters on the specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression('esaggs_other_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(11134); + }); + + it('filters on multiple specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression( + 'esaggs_multiple_timefields', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(7452); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 031a0e3576ccc..9590f9f8c1794 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./esaggs')); }); } diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 67d88b308ed91..951ba8e22d885 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/vars/workers.groovy b/vars/workers.groovy index 1c55c676d9425..b4e4a115f2011 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -57,6 +57,26 @@ def base(Map params, Closure closure) { // Try to clone from Github up to 8 times, waiting 15 secs between attempts retryWithDelay(8, 15) { scmVars = checkout scm + + def mergeBase + if (env.ghprbTargetBranch) { + sh( + script: "cd kibana && git fetch origin ${env.ghprbTargetBranch}", + label: "update reference to target branch 'origin/${env.ghprbTargetBranch}'" + ) + mergeBase = sh( + script: "cd kibana && git merge-base HEAD FETCH_HEAD", + label: "determining merge point with target branch 'origin/${env.ghprbTargetBranch}'", + returnStdout: true + ).trim() + } + + ciStats.reportGitInfo( + env.ghprbSourceBranch ?: scmVars.GIT_LOCAL_BRANCH ?: scmVars.GIT_BRANCH, + scmVars.GIT_COMMIT, + env.ghprbTargetBranch, + mergeBase + ) } } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index e276786092e74..9f43bf8da0601 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -37,14 +37,14 @@ "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", - "xpack.siem": ["plugins/siem", "legacy/plugins/siem"], + "xpack.siem": "plugins/siem", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", - "xpack.uptime": ["plugins/uptime", "legacy/plugins/uptime"], + "xpack.uptime": ["plugins/uptime"], "xpack.watcher": "plugins/watcher" }, "translations": [ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index af5ace8e3cd3b..4f1251321b005 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 e65f6cf60928e..cfadddac3994a 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,20 +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 { 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 [ @@ -39,17 +31,9 @@ module.exports = function(kibana) { apm(kibana), maps(kibana), canvas(kibana), - indexManagement(kibana), infra(kibana), taskManager(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 276d1ea3accea..0000000000000 --- a/x-pack/legacy/plugins/actions/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './server/'; diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts deleted file mode 100644 index 63dd6f99f9c24..0000000000000 --- 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 0d0a698841269..0000000000000 --- a/x-pack/legacy/plugins/alerting/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './server'; diff --git a/x-pack/legacy/plugins/alerting/server/index.ts b/x-pack/legacy/plugins/alerting/server/index.ts deleted file mode 100644 index 065af7dedebd9..0000000000000 --- 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 eb71994bd2e47..e9942a327b69e 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 27095264461ee..f57ddb5cf69a2 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 a5d8902ff1626..f01b2aa335a3a 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 858e6d29bfa5e..2be3c82a8385b 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 1e1d49b2cf417..d499dddeeb8b3 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/public/components/app/track_route_change.js b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js index e837f5200a159..2886aa868eb9e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js +++ b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js @@ -7,13 +7,17 @@ import { get } from 'lodash'; import { getWindow } from '../../lib/get_window'; import { CANVAS_APP } from '../../../common/lib/constants'; -import { getCoreStart, getStartPlugins } from '../../legacy'; +import { platformService } from '../../services'; export function trackRouteChange() { - const basePath = getCoreStart().http.basePath.get(); - // storage.set(LOCALSTORAGE_LASTPAGE, pathname); - getStartPlugins().__LEGACY.trackSubUrlForApp( - CANVAS_APP, - getStartPlugins().__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) - ); + const basePath = platformService.getService().coreStart.http.basePath.get(); + + platformService + .getService() + .startPlugins.__LEGACY.trackSubUrlForApp( + CANVAS_APP, + platformService + .getService() + .startPlugins.__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) + ); } diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index a416adfe77469..5c420cf3a04c9 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 5bb628909c32e..f83887bbcbdfd 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 834d5868c35ea..57b513affd781 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { ChromeBreadcrumb } from '../../../../../../src/core/public'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getBaseBreadcrumb = () => ({ text: 'Canvas', @@ -24,6 +24,6 @@ export const getWorkpadBreadcrumb = ({ }; export const setBreadcrumb = (paths: ChromeBreadcrumb | ChromeBreadcrumb[]) => { - const setBreadCrumbs = getCoreStart().chrome.setBreadcrumbs; + const setBreadCrumbs = platformService.getService().coreStart.chrome.setBreadcrumbs; setBreadCrumbs(Array.isArray(paths) ? paths : [paths]); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts index 4118bb81b8363..8952802dc2f2b 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts @@ -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 40e09ee39d714..6430f7d87d4f7 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getDocumentationLinks = () => ({ - canvas: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + canvas: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/canvas.html`, - numeral: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + numeral: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/guide/numeral.html`, }); diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 6aa4968f29155..184f4f3c8af7c 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 f3681f50c56a5..e6628399f53c2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -11,7 +11,7 @@ import { DEFAULT_WORKPAD_CSS, } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; /* Remove any top level keys from the workpad which will be rejected by validation */ @@ -43,17 +43,17 @@ const sanitizeWorkpad = function(workpad) { }; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD}`; }; const getApiPathStructures = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`; }; const getApiPathAssets = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`; }; diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 36ce1974be272..baeb4ebd453d2 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 12c0a687bf308..17d836f1441c9 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 0000000000000..440e9523044c1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/platform.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CanvasServiceFactory } from '.'; +import { CoreStart, CoreSetup, CanvasSetupDeps, CanvasStartDeps } from '../plugin'; + +interface PlatformService { + coreSetup: CoreSetup; + coreStart: CoreStart; + setupPlugins: CanvasSetupDeps; + startPlugins: CanvasStartDeps; +} + +export const platformServiceFactory: CanvasServiceFactory = ( + 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 40c017543147f..bfa68b33908e0 100644 --- a/x-pack/legacy/plugins/canvas/public/state/initial_state.js +++ b/x-pack/legacy/plugins/canvas/public/state/initial_state.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -13,7 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: getCoreStart().application.capabilities.canvas.save, + canUserWrite: platformService.getService().coreStart.application.capabilities.canvas.save, zoomScale: 1, elementStats: { total: 0, diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js index 12733680ed32d..30f9c638a054f 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js @@ -5,7 +5,7 @@ */ import { handleActions } from 'redux-actions'; -import { getCoreStart } from '../../legacy'; +import { platformService } from '../../services'; import { getDefaultWorkpad } from '../defaults'; import { setWorkpad, @@ -22,11 +22,13 @@ import { APP_ROUTE_WORKPAD } from '../../../common/lib/constants'; export const workpadReducer = handleActions( { [setWorkpad]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${payload.id}`, - payload.name, - payload.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${payload.id}`, + payload.name, + payload.id + ); return payload; }, @@ -39,11 +41,13 @@ export const workpadReducer = handleActions( }, [setName]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${workpadState.id}`, - payload, - workpadState.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${workpadState.id}`, + payload, + workpadState.id + ); return { ...workpadState, name: payload }; }, diff --git a/x-pack/legacy/plugins/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts deleted file mode 100644 index afca15203b970..0000000000000 --- a/x-pack/legacy/plugins/index_management/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// TODO: Remove this once CCR is migrated to the plugins directory. -export function indexManagement(kibana: any) { - return new kibana.Plugin({ - id: 'index_management', - configPrefix: 'xpack.index_management', - }); -} diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d1e8892fa2c98..a1186e04ee27a 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 1b1fbf111fe04..bb1a6b74d43a7 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 3a4c7b71dcd03..36030e1fa7f2a 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 a047c25c2b1d7..c6031cb220334 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 baad348d7a136..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/common/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const PLUGIN = { - ID: 'remoteClusters', -}; diff --git a/x-pack/legacy/plugins/remote_clusters/index.ts b/x-pack/legacy/plugins/remote_clusters/index.ts deleted file mode 100644 index 439cb818d8a56..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { PLUGIN } from './common'; - -export function remoteClusters(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.remote_clusters', - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - // TODO: Remove once CCR has migrated to NP - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled') - ); - }, - init() {}, - }); -} diff --git a/x-pack/legacy/plugins/remote_clusters/public/index.scss b/x-pack/legacy/plugins/remote_clusters/public/index.scss deleted file mode 100644 index 4ae11323642d8..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/public/index.scss +++ /dev/null @@ -1,28 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Remote clusters plugin styles - -// Prefix all styles with "remoteClusters" to avoid conflicts. -// Examples -// remoteClustersChart -// remoteClustersChart__legend -// remoteClustersChart__legend--small -// remoteClustersChart__legend-isLoading - -/** - * 1. Override EuiFormRow styles. Otherwise the switch will jump around when toggled on and off, - * as the 'Reset to defaults' link is added to and removed from the DOM. - * 2. Fix the positioning. - */ -.remoteClusterSkipIfUnavailableSwitch { - justify-content: flex-start !important; /* 1 */ - padding-top: $euiSizeS !important; -} - -/** - * 1. Prevent inherited flexbox layout from compressing this element on IE. - */ - .remoteClustersConnectionStatus__message { - flex-basis: auto !important; /* 1 */ -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 5a04f1a497abf..0d75b067fbe63 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { screenshotsObservableFactory } from './observable'; +export { screenshotsObservableFactory, ScreenshotsObservableFn } from './observable'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 519a3289395b9..c6861ae1d17ad 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -21,10 +21,18 @@ import { waitForVisualizations } from './wait_for_visualizations'; const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; +export type ScreenshotsObservableFn = ({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, +}: ScreenshotObservableOpts) => Rx.Observable; + 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 c9cba64a732b6..9d3deda5d98be 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -5,14 +5,14 @@ */ import * as Rx from 'rxjs'; -import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; -import { cryptoFactory } from '../../../../server/lib/crypto'; -import { executeJobFactory } from './index'; -import { generatePngObservableFactory } from '../lib/generate_png'; import { CancellationToken } from '../../../../common/cancellation_token'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { cryptoFactory } from '../../../../server/lib/crypto'; +import { createMockReportingCore } from '../../../../test_helpers'; import { JobDocPayloadPNG } from '../../types'; +import { generatePngObservableFactory } from '../lib/generate_png'; +import { executeJobFactory } from './index'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); @@ -31,8 +31,6 @@ const mockLoggerFactory = { }; const getMockLogger = () => new LevelLogger(mockLoggerFactory); -const captureConfig = {} as CaptureConfig; - const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { const crypto = cryptoFactory(mockEncryptionKey); @@ -46,10 +44,13 @@ beforeEach(async () => { 'server.basePath': '/sbp', }; const reportingConfig = { + index: '.reporting-2018.10.10', encryptionKey: mockEncryptionKey, 'kibanaServer.hostname': 'localhost', 'kibanaServer.port': 5601, 'kibanaServer.protocol': 'http', + 'queue.indexInterval': 'daily', + 'queue.timeout': Infinity, }; const mockReportingConfig = { get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], @@ -74,13 +75,8 @@ afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -94,26 +90,43 @@ test(`passes browserTimezone to generatePng`, async () => { cancellationToken ); - expect(generatePngObservable).toBeCalledWith( - expect.any(LevelLogger), - 'http://localhost:5601/sbp/app/kibana#/something', - browserTimezone, - expect.anything(), - undefined - ); + expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "PNG", + "execute", + "pngJobId", + ], + "warning": [Function], + }, + "http://localhost:5601/sbp/app/kibana#/something", + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + ], + ] + `); }); test(`returns content_type of application/png`, async () => { const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); const { content_type: contentType } = await executeJob( 'pngJobId', @@ -126,13 +139,8 @@ test(`returns content_type of application/png`, async () => { test(`returns content of generatePng getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 113da92d1862f..0ffd42d0b52f9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -25,13 +25,11 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut parentLogger: Logger ) { const config = reporting.getConfig(); - const captureConfig = config.get('capture'); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { - const browserDriverFactory = await reporting.getBrowserDriverFactory(); - const generatePngObservable = generatePngObservableFactory(captureConfig, browserDriverFactory); + const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index a15541d99f6fb..c03ea170f76ee 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -6,19 +6,15 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { CaptureConfig } from '../../../../server/types'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; +import { ConditionalHeaders } from '../../../../types'; import { LayoutParams } from '../../../common/layouts/layout'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; -export function generatePngObservableFactory( - captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -) { - const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); +export async function generatePngObservableFactory(reporting: ReportingCore) { + const getScreenshots = await reporting.getScreenshotsObservable(); return function generatePngObservable( logger: LevelLogger, @@ -32,7 +28,7 @@ export function generatePngObservableFactory( } const layout = new PreserveLayout(layoutParams.dimensions); - const screenshots$ = screenshotsObservable({ + const screenshots$ = getScreenshots({ logger, urls: [url], conditionalHeaders, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts index c3c0d38584bc1..ae18c0f4f9f4b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -5,11 +5,11 @@ */ import * as Rx from 'rxjs'; -import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; +import { createMockReportingCore } from '../../../../test_helpers'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { LevelLogger } from '../../../../server/lib'; import { CancellationToken } from '../../../../types'; -import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { ReportingCore } from '../../../../server'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { JobDocPayloadPDF } from '../../types'; import { executeJobFactory } from './index'; @@ -22,8 +22,6 @@ const cancellationToken = ({ on: jest.fn(), } as unknown) as CancellationToken; -const captureConfig = {} as CaptureConfig; - const mockLoggerFactory = { get: jest.fn().mockImplementation(() => ({ error: jest.fn(), @@ -72,16 +70,64 @@ beforeEach(async () => { afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); +test(`passes browserTimezone to generatePdf`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await executeJob( + 'pdfJobId', + getJobDocPayload({ + relativeUrl: '/app/kibana#/something', + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken + ); + + expect(generatePdfObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "printable_pdf", + "execute", + "pdfJobId", + ], + "warning": [Function], + }, + undefined, + Array [ + "http://localhost:5601/sbp/app/kibana#/something", + ], + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + false, + ], + ] + `); +}); + test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); const executeJob = await executeJobFactory(mockReporting, logger); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger); const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = generatePdfObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); const { content_type: contentType } = await executeJob( @@ -94,12 +140,7 @@ test(`returns content_type of application/pdf`, async () => { test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePdfObservable = generatePdfObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index dbdccb6160a6e..3d69042b6c7ab 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -26,14 +26,12 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut parentLogger: Logger ) { const config = reporting.getConfig(); - const captureConfig = config.get('capture'); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { - const browserDriverFactory = await reporting.getBrowserDriverFactory(); - const generatePdfObservable = generatePdfObservableFactory(captureConfig, browserDriverFactory); + const generatePdfObservable = await generatePdfObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index a62b7ec7013a5..c882ef682f952 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -7,12 +7,11 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { CaptureConfig } from '../../../../server/types'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; +import { ConditionalHeaders } from '../../../../types'; import { createLayout } from '../../../common/layouts'; import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; // @ts-ignore untyped module import { pdf } from './pdf'; @@ -27,11 +26,10 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export function generatePdfObservableFactory( - captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -) { - const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); +export async function generatePdfObservableFactory(reporting: ReportingCore) { + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const getScreenshots = await reporting.getScreenshotsObservable(); return function generatePdfObservable( logger: LevelLogger, @@ -43,7 +41,7 @@ export function generatePdfObservableFactory( logo?: string ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { const layout = createLayout(captureConfig, layoutParams) as LayoutInstance; - const screenshots$ = screenshotsObservable({ + const screenshots$ = getScreenshots({ logger, urls, conditionalHeaders, diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 9be61d091b00e..0b243f13adb80 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -24,6 +24,10 @@ import { ReportingConfig, ReportingConfigType } from './config'; import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; import { registerRoutes } from './routes'; import { ReportingSetupDeps } from './types'; +import { + screenshotsObservableFactory, + ScreenshotsObservableFn, +} from '../export_types/common/lib/screenshots'; interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; @@ -95,13 +99,13 @@ export class ReportingCore { return (await this.getPluginStartDeps()).enqueueJob; } - public async getBrowserDriverFactory(): Promise { - 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 c564963e363cc..4288a37fe6adc 100644 --- a/x-pack/legacy/plugins/reporting/server/index.ts +++ b/x-pack/legacy/plugins/reporting/server/index.ts @@ -14,3 +14,5 @@ export const plugin = (context: PluginInitializerContext, config: ReportingConfi export { ReportingPlugin } from './plugin'; export { ReportingConfig, ReportingCore }; + +export { PreserveLayout, PrintLayout } from '../export_types/common/layouts'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 8230ee889ae05..560cc943ed45e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -16,21 +16,23 @@ export async function createQueueFactory( 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 5a062a693b468..3e87337dc4355 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -26,12 +26,12 @@ interface ConfirmedJob { } export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger): EnqueueJobFn { - const logger = parentLogger.clone(['queue-job']); const config = reporting.getConfig(); - const captureConfig = config.get('capture'); - const queueConfig = config.get('queue'); - const browserType = captureConfig.browser.type; - const maxAttempts = captureConfig.maxAttempts; + const queueTimeout = config.get('queue', 'timeout'); + const browserType = config.get('capture', 'browser', 'type'); + const maxAttempts = config.get('capture', 'maxAttempts'); + + const logger = parentLogger.clone(['queue-job']); return async function enqueueJob( exportTypeId: string, @@ -53,7 +53,7 @@ export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger const payload = await createJob(jobParams, headers, request); const options = { - timeout: queueConfig.timeout, + timeout: queueTimeout, created_by: get(user, 'username', false), browser_type: browserType, max_attempts: maxAttempts, diff --git a/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap new file mode 100644 index 0000000000000..aa22c3f66df18 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`data modeling with empty data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, +} +`; + +exports[`data modeling with normal looking usage data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 3, + }, + "_all": 12, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 6, + "dashboard": 0, + "visualization": 3, + }, + "available": true, + "layout": Object { + "preserve_layout": 9, + "print": 0, + }, + "total": 9, + }, + "status": Object { + "completed": 10, + "completed_with_warnings": 1, + "failed": 1, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "visualization": 1, + }, + "printable_pdf": Object { + "canvas workpad": 6, + "visualization": 3, + }, + }, + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + "failed": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, +} +`; + +exports[`data modeling with sparse data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 1, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, +} +`; diff --git a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts index 359bcc45230c3..ef985d2dd1cf3 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts @@ -6,7 +6,7 @@ import { uniq } from 'lodash'; import { CSV_JOB_TYPE, PDF_JOB_TYPE, PNG_JOB_TYPE } from '../../common/constants'; -import { AvailableTotal, FeatureAvailabilityMap, RangeStats, ExportType } from './types'; +import { AvailableTotal, ExportType, FeatureAvailabilityMap, RangeStats } from './types'; function getForFeature( range: Partial, @@ -47,6 +47,7 @@ export const decorateRangeStats = ( const { _all: rangeAll, status: rangeStatus, + statuses: rangeStatusByApp, [PDF_JOB_TYPE]: rangeStatsPdf, ...rangeStatsBasic } = rangeStats; @@ -73,6 +74,7 @@ export const decorateRangeStats = ( const resultStats = { _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, + statuses: rangeStatusByApp, ...rangePdf, ...rangeBasic, } as RangeStats; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index e9523d9e70202..2c3bb8f4bf71c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -11,13 +11,13 @@ import { ReportingConfig } from '../types'; import { decorateRangeStats } from './decorate_range_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import { - AggregationBuckets, - AggregationResults, + AggregationResultBuckets, FeatureAvailabilityMap, JobTypes, KeyCountBucket, - RangeAggregationResults, RangeStats, + SearchResponse, + StatusByAppBucket, } from './types'; type XPackInfo = XPackMainPlugin['info']; @@ -29,6 +29,7 @@ const LAYOUT_TYPES_FIELD = 'meta.layout.keyword'; const OBJECT_TYPES_KEY = 'objectTypes'; const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; +const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; const DEFAULT_TERMS_SIZE = 10; @@ -38,16 +39,30 @@ const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; const getKeyCount = (buckets: KeyCountBucket[]): { [key: string]: number } => buckets.reduce((accum, { key, doc_count: count }) => ({ ...accum, [key]: count }), {}); -function getAggStats(aggs: AggregationResults) { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; - const jobTypes: JobTypes = jobBuckets.reduce( +// indexes some key/count buckets by statusType > jobType > appName: statusCount +const getAppStatuses = (buckets: StatusByAppBucket[]) => + buckets.reduce((statuses, statusBucket) => { + return { + ...statuses, + [statusBucket.key]: statusBucket.jobTypes.buckets.reduce((jobTypes, job) => { + return { + ...jobTypes, + [job.key]: job.appNames.buckets.reduce((apps, app) => { + return { + ...apps, + [app.key]: app.doc_count, + }; + }, {}), + }; + }, {}), + }; + }, {}); + +function getAggStats(aggs: AggregationResultBuckets): RangeStats { + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const jobTypes = jobBuckets.reduce( (accum: JobTypes, { key, doc_count: count }: { key: string; doc_count: number }) => { - return { - ...accum, - [key]: { - total: count, - }, - }; + return { ...accum, [key]: { total: count } }; }, {} as JobTypes ); @@ -55,8 +70,8 @@ function getAggStats(aggs: AggregationResults) { // merge pdf stats into pdf jobtype key const pdfJobs = jobTypes[PRINTABLE_PDF_JOBTYPE]; if (pdfJobs) { - const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); - const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); + const pdfAppBuckets = get(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 dbc674ce36ec8..61b736a3e4d8c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -12,6 +12,7 @@ import { getReportingUsageCollector, } from './reporting_usage_collector'; import { ReportingConfig } from '../types'; +import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -61,7 +62,7 @@ const getMockReportingConfig = () => ({ get: () => {}, kbnConfig: { get: () => '' }, }); -const getResponseMock = (customization = {}) => customization; +const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; @@ -206,212 +207,143 @@ describe('data modeling', () => { ranges: { buckets: { all: { - doc_count: 54, - layoutTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'preserve_layout', doc_count: 13 }, - { key: 'print', doc_count: 10 }, - ], - }, - }, - objectTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 23 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'pending', doc_count: 33 }, - { key: 'completed', doc_count: 20 }, - { key: 'processing', doc_count: 1 }, - ], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 27 }, - { key: 'printable_pdf', doc_count: 23 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - lastDay: { - doc_count: 11, - layoutTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'print', doc_count: 2 }], - }, - }, - objectTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 2 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 11 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 5 }, - { key: 'PNG', doc_count: 4 }, - { key: 'printable_pdf', doc_count: 2 }, - ], - }, + doc_count: 12, + jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, + layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, + objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, + statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, + statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, }, last7Days: { - doc_count: 27, - layoutTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'print', doc_count: 10 }, - { key: 'preserve_layout', doc_count: 3 }, - ], - }, - }, - objectTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 13 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 27 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'printable_pdf', doc_count: 13 }, - { key: 'csv', doc_count: 10 }, - { key: 'PNG', doc_count: 4 }, - ], - }, + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + }, + lastDay: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, }, }, }, }, - }) + } as SearchResponse) // prettier-ignore ) ); const usageStats = await fetch(callClusterMock as any); - expect(usageStats).toMatchInlineSnapshot(` - Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 54, - "available": true, - "browser_type": undefined, - "csv": Object { - "available": true, - "total": 27, - }, - "enabled": true, - "last7Days": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 27, - "csv": Object { - "available": true, - "total": 10, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 13, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 3, - "print": 10, + expect(usageStats).toMatchSnapshot(); + }); + + test('with sparse data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + last7Days: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + lastDay: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + }, }, - "total": 13, }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 27, - }, - }, - "lastDay": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 11, - "csv": Object { - "available": true, - "total": 5, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 2, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 0, - "print": 2, + } as SearchResponse) // prettier-ignore + ) + ); + + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); + }); + + test('with empty data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + last7Days: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + lastDay: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + }, }, - "total": 2, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 11, - }, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 23, - "visualization": 0, }, - "available": true, - "layout": Object { - "preserve_layout": 13, - "print": 10, - }, - "total": 23, - }, - "status": Object { - "completed": 20, - "failed": 0, - "pending": 33, - "processing": 1, - }, - } - `); + } as SearchResponse) + ) + ); + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts index 98e025ccf661e..83f1701863355 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AvailableTotal { - available: boolean; - total: number; -} - -interface StatusCounts { - [statusType: string]: number; -} - export interface KeyCountBucket { key: string; doc_count: number; @@ -20,22 +11,53 @@ export interface KeyCountBucket { export interface AggregationBuckets { buckets: KeyCountBucket[]; - pdf?: { - buckets: KeyCountBucket[]; - }; } -/* - * Mapped Types and Intersection Types - */ +export interface StatusByAppBucket { + key: string; + doc_count: number; + jobTypes: { + buckets: Array<{ + doc_count: number; + key: string; + appNames: AggregationBuckets; + }>; + }; +} -type AggregationKeys = 'jobTypes' | 'layoutTypes' | 'objectTypes' | 'statusTypes'; -export type AggregationResults = { [K in AggregationKeys]: AggregationBuckets } & { +export interface AggregationResultBuckets { + jobTypes: AggregationBuckets; + layoutTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + objectTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + statusTypes: AggregationBuckets; + statusByApp: { + buckets: StatusByAppBucket[]; + }; doc_count: number; -}; +} -type RangeAggregationKeys = 'all' | 'lastDay' | 'last7Days'; -export type RangeAggregationResults = { [K in RangeAggregationKeys]?: AggregationResults }; +export interface SearchResponse { + aggregations: { + ranges: { + buckets: { + all: AggregationResultBuckets; + last7Days: AggregationResultBuckets; + lastDay: AggregationResultBuckets; + }; + }; + }; +} + +export interface AvailableTotal { + available: boolean; + total: number; +} type BaseJobTypeKeys = 'csv' | 'PNG'; export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { @@ -51,9 +73,22 @@ export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { }; }; +interface StatusCounts { + [statusType: string]: number; +} + +interface StatusByAppCounts { + [statusType: string]: { + [jobType: string]: { + [appName: string]: number; + }; + }; +} + export type RangeStats = JobTypes & { _all: number; status: StatusCounts; + statuses: StatusByAppCounts; }; export type ExportType = 'csv' | 'printable_pdf' | 'PNG'; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 5b2218af1fd52..b1dec2ce82c52 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -78,9 +78,7 @@ export const security = (kibana: Record) => // features are up to date. xpackInfo .feature(this.id) - .registerLicenseCheckResultsGenerator(() => - securityPlugin.__legacyCompat.license.getFeatures() - ); + .registerLicenseCheckResultsGenerator(() => securityPlugin.license.getFeatures()); server.expose({ getUser: async (request: LegacyRequest) => diff --git a/x-pack/legacy/plugins/siem/.gitattributes b/x-pack/legacy/plugins/siem/.gitattributes deleted file mode 100644 index a4071d39e63c0..0000000000000 --- a/x-pack/legacy/plugins/siem/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -# Auto-collapse generated files in GitHub -# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github -x-pack/legacy/plugins/siem/public/graphql/types.ts linguist-generated=true -x-pack/legacy/plugins/siem/public/graphql/introspection.json linguist-generated=true - diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts deleted file mode 100644 index d572561944a76..0000000000000 --- 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 c9b3f8a37a7ea..0000000000000 --- 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 01175a98d1e44..0000000000000 --- a/x-pack/legacy/plugins/siem/public/app/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { CoreStart, StartPlugins, AppMountParameters } from '../plugin'; -import { SiemApp } from './app'; - -export const renderApp = ( - core: CoreStart, - plugins: StartPlugins, - { element }: AppMountParameters -) => { - render(, element); - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx deleted file mode 100644 index 778adc708d901..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useEffect, useCallback, useMemo } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../plugins/siem/common/constants'; -import { AlertsComponentsQueryProps } from './types'; -import { AlertsTable } from './alerts_table'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogramContainer } from '../matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; -const ID = 'alertsOverTimeQuery'; - -export const AlertsView = ({ - deleteQuery, - endDate, - filterQuery, - pageFilters, - setQuery, - startDate, - type, -}: AlertsComponentsQueryProps) => { - const [defaultNumberFormat] = useUiSetting$(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 a24c66e31e670..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; -import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; -import { MatrixHistogramOption } from '../matrix_histogram/types'; - -type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; -export interface AlertsComponentsQueryProps - extends Pick< - CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' - > { - pageFilters: Filter[]; - stackByOptions?: MatrixHistogramOption[]; - defaultFilters?: Filter[]; - defaultStackByOption?: MatrixHistogramOption; -} diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx deleted file mode 100644 index e2078bb2473f5..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBadge } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -const RoundedBadge = styled(EuiBadge)` - align-items: center; - border-radius: 100%; - display: inline-flex; - font-size: 9px; - height: 34px; - justify-content: center; - margin: 0 5px 0 5px; - padding: 7px 6px 4px 6px; - user-select: none; - width: 34px; - - .euiBadge__content { - position: relative; - top: -1px; - } - - .euiBadge__text { - text-overflow: clip; - } -` 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 55e114818ffea..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFieldSearch } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { - QuerySuggestion, - QuerySuggestionTypes, -} from '../../../../../../../src/plugins/data/public'; - -import { TestProviders } from '../../mock'; - -import { AutocompleteField } from '.'; - -const mockAutoCompleteData: QuerySuggestion[] = [ - { - type: QuerySuggestionTypes.Field, - text: 'agent.ephemeral_id ', - description: - '

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 f051e18f8acab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiFieldSearch, - EuiFieldSearchProps, - EuiOutsideClickDetector, - EuiPanel, -} from '@elastic/eui'; -import React from 'react'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; - -import euiStyled from '../../../../../common/eui_styled_components'; - -import { SuggestionItem } from './suggestion_item'; - -interface AutocompleteFieldProps { - 'data-test-subj'?: string; - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onSubmit?: (value: string) => void; - onChange?: (value: string) => void; - placeholder?: string; - suggestions: QuerySuggestion[]; - value: string; -} - -interface AutocompleteFieldState { - areSuggestionsVisible: boolean; - isFocused: boolean; - selectedIndex: number | null; -} - -export class AutocompleteField extends React.PureComponent< - AutocompleteFieldProps, - AutocompleteFieldState -> { - public readonly state: AutocompleteFieldState = { - areSuggestionsVisible: false, - isFocused: false, - selectedIndex: null, - }; - - private inputElement: HTMLInputElement | null = null; - - public render() { - const { - 'data-test-subj': dataTestSubj, - suggestions, - isLoadingSuggestions, - isValid, - placeholder, - value, - } = this.props; - const { areSuggestionsVisible, selectedIndex } = this.state; - return ( - - - - {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 e18f9b0d346ad..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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 216fe9105327c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RenderTooltipContentParams } from '../../../../maps/public'; -import { inputsModel } from '../../store/inputs'; - -export interface IndexPatternMapping { - title: string; - id: string; -} - -export interface LayerMappingDetails { - metricField: string; - geoField: string; - tooltipProperties: string[]; - label: string; -} - -export interface LayerMapping { - source: LayerMappingDetails; - destination: LayerMappingDetails; -} - -export interface LayerMappingCollection { - [indexPatternTitle: string]: LayerMapping; -} - -export type SetQuery = (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; -}) => void; - -export interface MapFeature { - id: number; - layerId: string; -} - -export interface LoadFeatureProps { - layerId: string; - featureId: number; -} - -export interface FeatureProperty { - _propertyKey: string; - _rawValue: string | string[]; -} - -export interface FeatureGeometry { - coordinates: [number]; - type: string; -} - -export type MapToolTipProps = Partial; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx deleted file mode 100644 index c6d9dbc2fcfc8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { - ColumnHeaderOptions, - SubsetTimelineModel, - TimelineModel, -} from '../../store/timeline/model'; -import { OnChangeItemsPerPage } from '../timeline/events'; -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { useUiSetting } from '../../lib/kibana'; -import { EventsViewer } from './events_viewer'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; -import { TimelineTypeContextProps } from '../timeline/timeline_context'; -import { InspectButtonContainer } from '../inspect'; -import * as i18n from './translations'; - -export interface OwnProps { - defaultIndices?: string[]; - defaultModel: SubsetTimelineModel; - end: number; - id: string; - start: number; - headerFilterGroup?: React.ReactNode; - pageFilters?: Filter[]; - timelineTypeContext?: TimelineTypeContextProps; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; -} - -type Props = OwnProps & PropsFromRedux; - -const defaultTimelineTypeContext = { - loadingText: i18n.LOADING_EVENTS, -}; - -const StatefulEventsViewerComponent: React.FC = ({ - 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 b0f6494e2d663..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBadge } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import styled from 'styled-components'; - -import { State, timelineSelectors } from '../../store'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { FlyoutButton } from './button'; -import { Pane } from './pane'; -import { timelineActions } from '../../store/actions'; -import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; -import { StatefulTimeline } from '../timeline'; -import { TimelineById } from '../../store/timeline/types'; - -export const Badge = styled(EuiBadge)` - position: absolute; - padding-left: 4px; - padding-right: 4px; - right: 0%; - top: 0%; - border-bottom-left-radius: 5px; -` 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 abde602c1bdac..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_BYTES_FORMAT } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -type Bytes = string | number; - -export const formatBytes = (value: Bytes, format: string) => { - return numeral(value).format(format); -}; - -export const useFormatBytes = () => { - const [bytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); - - return (value: Bytes) => formatBytes(value, bytesFormat); -}; - -export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( - <>{useFormatBytes()(value)} -); - -PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; - -export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); - -PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx deleted file mode 100644 index 56fa0d56f3c3a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import '../../mock/match_media'; -import { HeaderGlobal } from './index'; - -jest.mock('react-router-dom', () => ({ - useLocation: () => ({ - pathname: '/app/siem#/hosts/allHosts', - hash: '', - search: '', - state: '', - }), - withRouter: () => jest.fn(), -})); - -jest.mock('ui/new_platform'); - -// Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar -jest.mock('../search_bar', () => ({ - SiemSearchBar: () => null, -})); - -describe('HeaderGlobal', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - test('it renders', () => { - const wrapper = shallow(); - - 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 acbb337a7c2ab..0000000000000 --- 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 45225e31e9ac8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { isNil } from 'lodash/fp'; -import styled from 'styled-components'; - -import { IP_REPUTATION_LINKS_SETTING } from '../../../../../../plugins/siem/common/constants'; -import { - DefaultFieldRendererOverflow, - DEFAULT_MORE_MAX_HEIGHT, -} from '../field_renderers/field_renderers'; -import { encodeIpv6 } from '../../lib/helpers'; -import { - getCaseDetailsUrl, - getHostDetailsUrl, - getIPDetailsUrl, - getCreateCaseUrl, -} from '../link_to'; -import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; -import { useUiSetting$ } from '../../lib/kibana'; -import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { navTabs } from '../../pages/home/home_navigations'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; - -import * as i18n from './translations'; - -export const DEFAULT_NUMBER_OF_LINK = 5; - -// Internal Links -const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ - children, - hostName, -}) => ( - <EuiLink href={getHostDetailsUrl(encodeURIComponent(hostName))}> - {children ? children : hostName} - </EuiLink> -); - -const whitelistUrlSchemes = ['http://', 'https://']; -export const ExternalLink = React.memo<{ - url: string; - children?: React.ReactNode; - idx?: number; - overflowIndexStart?: number; - allItemsLimit?: number; -}>( - ({ - url, - children, - idx, - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - }) => { - const lastVisibleItemIndex = overflowIndexStart - 1; - const lastItemIndex = allItemsLimit - 1; - const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); - const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); - return url && inWhitelist && !isUrlInvalid(url) && children ? ( - <EuiToolTip content={url} position="top" data-test-subj="externalLinkTooltip"> - <EuiLink href={url} target="_blank" rel="noopener" data-test-subj="externalLink"> - {children} - <ExternalLinkIcon data-test-subj="externalLinkIcon" /> - {!isNil(idx) && idx < lastIndexToShow && <Comma data-test-subj="externalLinkComma" />} - </EuiLink> - </EuiToolTip> - ) : null; - } -); - -ExternalLink.displayName = 'ExternalLink'; - -export const HostDetailsLink = React.memo(HostDetailsLinkComponent); - -const IPDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - ip: string; - flowTarget?: FlowTarget | FlowTargetSourceDest; -}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( - <EuiLink href={`${getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget)}`}> - {children ? children : ip} - </EuiLink> -); - -export const IPDetailsLink = React.memo(IPDetailsLinkComponent); - -const CaseDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - detailName: string; - title?: string; -}> = ({ children, detailName, title }) => { - const search = useGetUrlSearch(navTabs.case); - - return ( - <EuiLink - href={getCaseDetailsUrl({ id: detailName, search })} - data-test-subj="case-details-link" - aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)} - > - {children ? children : detailName} - </EuiLink> - ); -}; -export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); -CaseDetailsLink.displayName = 'CaseDetailsLink'; - -export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { - const search = useGetUrlSearch(navTabs.case); - return <EuiLink href={getCreateCaseUrl(search)}>{children}</EuiLink>; -}); - -CreateCaseLink.displayName = 'CreateCaseLink'; - -// External Links -export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( - ({ children, link }) => ( - <ExternalLink url={`https://www.google.com/search?q=${encodeURIComponent(link)}`}> - {children ? children : link} - </ExternalLink> - ) -); - -GoogleLink.displayName = 'GoogleLink'; - -export const PortOrServiceNameLink = React.memo<{ - children?: React.ReactNode; - portOrServiceName: number | string; -}>(({ children, portOrServiceName }) => ( - <EuiLink - data-test-subj="port-or-service-name-link" - href={`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent( - String(portOrServiceName) - )}`} - target="_blank" - > - {children ? children : portOrServiceName} - </EuiLink> -)); - -PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; - -export const Ja3FingerprintLink = React.memo<{ - children?: React.ReactNode; - ja3Fingerprint: string; -}>(({ children, ja3Fingerprint }) => ( - <EuiLink - data-test-subj="ja3-fingerprint-link" - href={`https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`} - target="_blank" - > - {children ? children : ja3Fingerprint} - </EuiLink> -)); - -Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; - -export const CertificateFingerprintLink = React.memo<{ - children?: React.ReactNode; - certificateFingerprint: string; -}>(({ children, certificateFingerprint }) => ( - <EuiLink - data-test-subj="certificate-fingerprint-link" - href={`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent( - certificateFingerprint - )}`} - target="_blank" - > - {children ? children : certificateFingerprint} - </EuiLink> -)); - -CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; - -enum DefaultReputationLink { - 'virustotal.com' = 'virustotal.com', - 'talosIntelligence.com' = 'talosIntelligence.com', -} - -export interface ReputationLinkSetting { - name: string; - url_template: string; -} - -function isDefaultReputationLink(name: string): name is DefaultReputationLink { - return ( - name === DefaultReputationLink['virustotal.com'] || - name === DefaultReputationLink['talosIntelligence.com'] - ); -} -const isReputationLink = ( - rowItem: string | ReputationLinkSetting -): rowItem is ReputationLinkSetting => - (rowItem as ReputationLinkSetting).url_template !== undefined && - (rowItem as ReputationLinkSetting).name !== undefined; - -export const Comma = styled('span')` - margin-right: 5px; - margin-left: 5px; - &::after { - content: ' ,'; - } -`; - -Comma.displayName = 'Comma'; - -const defaultNameMapping: Record<DefaultReputationLink, string> = { - [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, - [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, -}; - -const ReputationLinkComponent: React.FC<{ - overflowIndexStart?: number; - allItemsLimit?: number; - showDomain?: boolean; - domain: string; - direction?: 'row' | 'column'; -}> = ({ - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - showDomain = false, - domain, - direction = 'row', -}) => { - const [ipReputationLinksSetting] = useUiSetting$<ReputationLinkSetting[]>( - IP_REPUTATION_LINKS_SETTING - ); - - const ipReputationLinks: ReputationLinkSetting[] = useMemo( - () => - ipReputationLinksSetting - ?.slice(0, allItemsLimit) - .filter( - ({ url_template, name }) => - !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) - ) - .map(({ name, url_template }: { name: string; url_template: string }) => ({ - name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, - url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), - })), - [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] - ); - - return ipReputationLinks?.length > 0 ? ( - <section> - <EuiFlexGroup - gutterSize="none" - justifyContent="center" - direction={direction} - alignItems="center" - data-test-subj="reputationLinkGroup" - > - <EuiFlexItem grow={true}> - {ipReputationLinks - ?.slice(0, overflowIndexStart) - .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( - <ExternalLink - allItemsLimit={ipReputationLinks.length} - idx={id} - overflowIndexStart={overflowIndexStart} - url={urlTemplate} - data-test-subj="externalLinkComponent" - key={`reputationLink-${id}`} - > - <>{showDomain ? domain : name ?? domain}</> - </ExternalLink> - ))} - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <DefaultFieldRendererOverflow - rowItems={ipReputationLinks} - idPrefix="moreReputationLink" - render={rowItem => { - return ( - isReputationLink(rowItem) && ( - <ExternalLink - url={rowItem.url_template} - overflowIndexStart={overflowIndexStart} - allItemsLimit={allItemsLimit} - > - <>{rowItem.name ?? domain}</> - </ExternalLink> - ) - ); - }} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={overflowIndexStart} - /> - </EuiFlexItem> - </EuiFlexGroup> - </section> - ) : null; -}; - -ReputationLinkComponent.displayName = 'ReputationLinkComponent'; - -export const ReputationLink = React.memo(ReputationLinkComponent); - -export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( - ({ children, domain }) => ( - <ExternalLink url={`https://www.iana.org/whois?q=${encodeURIComponent(domain)}`}> - {children ? children : domain} - </ExternalLink> - ) -); - -WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 5aa846d15b684..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`; - -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts deleted file mode 100644 index 98437845a3ab7..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiTitleSize } from '@elastic/eui'; -import { ScaleType, Position, TickFormatter } from '@elastic/charts'; -import { ActionCreator } from 'redux'; -import { ESQuery } from '../../../../../../plugins/siem/common/typed_json'; -import { SetQuery } from '../../pages/hosts/navigation/types'; -import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../graphql/types'; -import { UpdateDateRange } from '../charts/common'; - -export type MatrixHistogramMappingTypes = Record< - string, - { key: string; value: null; color?: string | undefined } ->; -export interface MatrixHistogramOption { - text: string; - value: string; -} - -export type GetSubTitle = (count: number) => string; -export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; - -export interface MatrixHisrogramConfigs { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - titleSize?: EuiTitleSize; -} - -interface MatrixHistogramBasicProps { - chartHeight?: number; - defaultIndex: string[]; - defaultStackByOption: MatrixHistogramOption; - dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - endDate: number; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - id: string; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - panelHeight?: number; - setQuery: SetQuery; - startDate: number; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title?: string | GetTitle; - titleSize?: EuiTitleSize; -} - -export interface MatrixHistogramQueryProps { - endDate: number; - errorMessage: string; - filterQuery?: ESQuery | string | undefined; - setAbsoluteRangeDatePicker?: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - stackByField: string; - startDate: number; - indexToAdd?: string[] | null; - isInspected: boolean; - histogramType: HistogramType; -} - -export interface MatrixHistogramProps extends MatrixHistogramBasicProps { - scaleType?: ScaleType; - yTickFormatter?: (value: number) => string; - showLegend?: boolean; - showSpacer?: boolean; - legendPosition?: Position; -} - -export interface HistogramBucket { - key_as_string: string; - key: number; - doc_count: number; -} -export interface GroupBucket { - key: string; - signals: { - buckets: HistogramBucket[]; - }; -} - -export interface HistogramAggregation { - histogramAgg: { - buckets: GroupBucket[]; - }; -} - -export interface BarchartConfigs { - series: { - xScaleType: ScaleType; - yScaleType: ScaleType; - stackAccessors: string[]; - }; - axis: { - xTickFormatter: TickFormatter; - yTickFormatter: TickFormatter; - tickSize: number; - }; - settings: { - legendPosition: Position; - onBrushEnd: UpdateDateRange; - showLegend: boolean; - showLegendExtra: boolean; - theme: { - scales: { - barsPadding: number; - }; - chartMargins: { - left: number; - right: number; - top: number; - bottom: number; - }; - chartPaddings: { - left: number; - right: number; - top: number; - bottom: number; - }; - }; - }; - customHeight: number; -} diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts deleted file mode 100644 index 991c82cf701e8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MlError } from '../ml/types'; -import { AuditMessageBase } from '../../../../../../plugins/ml/common/types/audit_message'; - -export interface Group { - id: string; - jobIds: string[]; - calendarIds: string[]; -} - -export interface CheckRecognizerProps { - indexPatternName: string[]; - signal: AbortSignal; -} - -export interface RecognizerModule { - id: string; - title: string; - query: Record<string, object>; - description: string; - logo: { - icon: string; - }; -} - -export interface GetModulesProps { - moduleId?: string; - signal: AbortSignal; -} - -export interface Module { - id: string; - title: string; - description: string; - type: string; - logoFile: string; - defaultIndexPattern: string; - query: Record<string, object>; - jobs: ModuleJob[]; - datafeeds: ModuleDatafeed[]; - kibana: object; -} - -/** - * Representation of an ML Job as returned from `the ml/modules/get_module` API - */ -export interface ModuleJob { - id: string; - config: { - groups: string[]; - description: string; - analysis_config: { - bucket_span: string; - summary_count_field_name?: string; - detectors: Detector[]; - influencers: string[]; - }; - analysis_limits: { - model_memory_limit: string; - }; - data_description: { - time_field: string; - time_format?: string; - }; - model_plot_config?: { - enabled: boolean; - }; - custom_settings: { - created_by: string; - custom_urls: CustomURL[]; - }; - job_type: string; - }; -} - -// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices -// See mockGetModuleResponse for examples -export interface ModuleDatafeed { - id: string; - config: { - job_id: string; - indexes?: string[]; - indices?: string[]; - query: Record<string, object>; - }; -} - -export interface MlSetupArgs { - configTemplate: string; - indexPatternName: string; - jobIdErrorFilter: string[]; - groups: string[]; - prefix?: string; -} - -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - -export interface Detector { - detector_description: string; - function: string; - by_field_name: string; - partition_field_name?: string; -} - -export interface CustomURL { - url_name: string; - url_value: string; -} - -/** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary - * that includes necessary metadata like moduleName, defaultIndexPattern, etc. - */ -export interface SiemJob extends JobSummary { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isInstalled: boolean; - isElasticJob: boolean; -} - -export interface AugmentedSiemJobFields { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isElasticJob: boolean; -} - -export interface SetupMlResponseJob { - id: string; - success: boolean; - error?: MlError; -} - -export interface SetupMlResponseDatafeed { - id: string; - success: boolean; - started: boolean; - error?: MlError; -} - -export interface SetupMlResponse { - jobs: SetupMlResponseJob[]; - datafeeds: SetupMlResponseDatafeed[]; - kibana: {}; -} - -export interface StartDatafeedResponse { - [key: string]: { - started: boolean; - error?: string; - }; -} - -export interface ErrorResponse { - statusCode?: number; - error?: string; - message?: string; -} - -export interface StopDatafeedResponse { - [key: string]: { - stopped: boolean; - }; -} - -export interface CloseJobsResponse { - [key: string]: { - closed: boolean; - }; -} - -export interface JobsFilters { - filterQuery: string; - showCustomJobs: boolean; - showElasticJobs: boolean; - selectedGroups: string[]; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts deleted file mode 100644 index 5407eba8b5b29..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr, omit } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { APP_NAME } from '../../../../../../../plugins/siem/common/constants'; -import { StartServices } from '../../../plugin'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; -import { SiemPageName } from '../../../pages/home/types'; -import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; -import { getOverviewUrl } from '../../link_to'; - -import { TabNavigationProps } from '../tab_navigation/types'; -import { getSearch } from '../helpers'; -import { SearchNavTab } from '../types'; - -export const setBreadcrumbs = ( - spyState: RouteSpyState & TabNavigationProps, - chrome: StartServices['chrome'] -) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState); - if (breadcrumbs) { - chrome.setBreadcrumbs(breadcrumbs); - } -}; - -export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ - { - text: APP_NAME, - href: getOverviewUrl(), - }, -]; - -const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.network; - -const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.hosts; - -const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SiemPageName.case; - -const isDetectionsRoutes = (spyState: RouteSpyState) => - spyState != null && spyState.pageName === SiemPageName.detections; - -export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps -): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', object); - if (isHostsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getHostDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isNetworkRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getIPDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isDetectionsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getDetectionRulesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isCaseRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getCaseDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if ( - spyState != null && - object.navTabs && - spyState.pageName && - object.navTabs[spyState.pageName] - ) { - return [ - ...siemRootBreadcrumb, - { - text: object.navTabs[spyState.pageName].name, - href: '', - }, - ]; - } - - return null; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts deleted file mode 100644 index 899d108fe246d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { Location } from 'history'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; -import { - replaceQueryStringInLocation, - replaceStateKeyInQueryString, - getQueryStringFromLocation, -} from '../url_state/helpers'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; - -import { SearchNavTab } from './types'; - -export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { - if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { - return URL_STATE_KEYS[tab.urlKey].reduce<Location>( - (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; - - if (urlKey === CONSTANTS.appQuery && urlState.query != null) { - if (urlState.query.query === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.query; - } - } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { - if (isEmpty(urlState.filters)) { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.filters; - } - } else if (urlKey === CONSTANTS.timerange) { - urlStateToReplace = urlState[CONSTANTS.timerange]; - } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { - const timeline = urlState[CONSTANTS.timeline]; - if (timeline.id === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = timeline; - } - } - return replaceQueryStringInLocation( - myLocation, - replaceStateKeyInQueryString( - urlKey, - urlStateToReplace - )(getQueryStringFromLocation(myLocation.search)) - ); - }, - { - pathname: '', - hash: '', - search: '', - state: '', - } - ).search; - } - return ''; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx deleted file mode 100644 index a821d310344d8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { CONSTANTS } from '../url_state/constants'; -import { SiemNavigationComponent } from './'; -import { setBreadcrumbs } from './breadcrumbs'; -import { navTabs } from '../../pages/home/home_navigations'; -import { HostsTableType } from '../../store/hosts/model'; -import { RouteSpyState } from '../../utils/route/types'; -import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; - -jest.mock('ui/new_platform'); -jest.mock('./breadcrumbs', () => ({ - setBreadcrumbs: jest.fn(), -})); - -describe('SIEM Navigation', () => { - const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs, - urlState: { - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }, - }; - const wrapper = mount(<SiemNavigationComponent {...mockProps} />); - test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - search: '', - tabName: 'authentications', - query: { query: '', language: 'kuery' }, - filters: [], - savedQuery: undefined, - timeline: { - id: '', - isOpen: false, - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); - test('it calls setBreadcrumbs with correct path on update', () => { - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - filters: [], - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - query: { language: 'kuery', query: '' }, - savedQuery: undefined, - search: '', - state: undefined, - tabName: 'authentications', - timeline: { id: '', isOpen: false }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx deleted file mode 100644 index b9563b60f301b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { navTabs } from '../../../pages/home/home_navigations'; -import { SiemPageName } from '../../../pages/home/types'; -import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; -import { HostsTableType } from '../../../store/hosts/model'; -import { RouteSpyState } from '../../../utils/route/types'; -import { CONSTANTS } from '../../url_state/constants'; -import { TabNavigationComponent } from './'; -import { TabNavigationProps } from './types'; - -jest.mock('ui/new_platform'); - -describe('Tab Navigation', () => { - const pageName = SiemPageName.hosts; - const hostName = 'siem-window'; - const tabName = HostsTableType.authentications; - const pathName = `/${pageName}/${hostName}/${tabName}`; - - describe('Page Navigation', () => { - const mockProps: TabNavigationProps & RouteSpyState = { - pageName, - pathName, - detailName: undefined, - search: '', - tabName, - navTabs, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); - expect(hostsTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); - expect(networkTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(networkTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); - expect(firstTab.props().href).toBe( - "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" - ); - }); - }); - - describe('Table Navigation', () => { - const mockHasMlUserPermissions = true; - const mockProps: TabNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const tableNavigationTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - - expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const tableNavigationTab = () => - wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); - expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: SiemPageName.hosts, - pathName: `/${SiemPageName.hosts}`, - tabName: HostsTableType.events, - }); - wrapper.update(); - expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const firstTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - expect(firstTab.props().href).toBe( - `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts deleted file mode 100644 index fe701ad115d17..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UrlInputsModel } from '../../../store/inputs/model'; -import { CONSTANTS } from '../../url_state/constants'; -import { HostsTableType } from '../../../store/hosts/model'; -import { TimelineUrl } from '../../../store/timeline/model'; -import { Filter, Query } from '../../../../../../../../src/plugins/data/public'; - -import { SiemNavigationProps } from '../types'; - -export interface TabNavigationProps extends SiemNavigationProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; -} - -export interface TabNavigationItemProps { - href: string; - hrefWithSearch: string; - id: string; - disabled: boolean; - name: string; - isSelected: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts deleted file mode 100644 index 96a70e0bc70cc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType } from '../../store/hosts/model'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS, UrlStateType } from '../url_state/constants'; - -export interface SiemNavigationProps { - display?: 'default' | 'condensed'; - navTabs: Record<string, NavTab>; -} - -export interface SiemNavigationComponentProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - urlState: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; - }; -} - -export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; - -export interface NavTab { - id: string; - name: string; - href: string; - disabled: boolean; - urlKey: UrlStateType; - isDetailPage?: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts deleted file mode 100644 index 686ec4e86e785..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../../../../plugins/siem/common/constants'; -import { KibanaServices } from '../../lib/kibana'; -import { rawNewsApiResponse } from '../../mock/news'; -import { rawNewsJSON } from '../../mock/raw_news'; - -import { - fetchNews, - getLocale, - getNewsFeedUrl, - getNewsItemsFromApiResponse, - removeSnapshotFromVersion, - showNewsItem, -} from './helpers'; -import { NewsItem, RawNewsApiResponse } from './types'; - -jest.mock('../../lib/kibana'); - -describe('helpers', () => { - describe('removeSnapshotFromVersion', () => { - test('it should remove an all-caps `-SNAPSHOT`', () => { - const version = '8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove a mixed-case `-SnApShoT`', () => { - const version = '8.0.0-SnApShoT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { - const version = '-SNAPSHOT8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { - const version = '8.0.0'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { - const version = '8.0.0SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); - }); - - test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { - const version = '8.0.0-SNAP'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); - }); - - test('it should NOT transform an undefined version', () => { - const version = undefined; - - expect(removeSnapshotFromVersion(version)).toBeUndefined(); - }); - - test('it should NOT transform an empty version', () => { - const version = ''; - - expect(removeSnapshotFromVersion(version)).toEqual(''); - }); - }); - - describe('getNewsFeedUrl', () => { - const getKibanaVersion = () => '8.0.0'; - - test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { - expect( - getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { - const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { - const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { - const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; - - expect( - getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { - const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; - - expect( - getNewsFeedUrl({ - newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, - getKibanaVersion: getKibanaVersionWithSnapshot, - }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - }); - - describe('getLocale', () => { - const fallback = 'wowzers'; - - test('it returns language specified in the document', () => { - const lang = 'ja'; - - document.documentElement.lang = lang; - - expect(getLocale(fallback)).toEqual(lang); - }); - - test('it returns the fallback when the language in the document is an empty string', () => { - document.documentElement.lang = ''; - - expect(getLocale(fallback)).toEqual(fallback); - }); - }); - - describe('getNewsItemsFromApiResponse', () => { - const expectedNewsItems: NewsItem[] = [ - { - description: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - expireOn: expect.any(Date), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Got SIEM Questions?', - }, - { - description: - 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', - expireOn: expect.any(Date), - hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Security 7.5.0 released', - }, - { - description: - 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', - expireOn: expect.any(Date), - hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Endpoint Security Overview Webinar', - }, - { - description: - 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', - expireOn: expect.any(Date), - hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Trying Elastic SIEM at Home?', - }, - { - description: - 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', - expireOn: expect.any(Date), - hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Introducing Elastic Endpoint Security', - }, - { - description: - 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', - expireOn: expect.any(Date), - hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'What is Elastic Common Schema (ECS)?', - }, - ]; - - test('it returns an empty collection of news items when the response is undefined', () => { - expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response is null', () => { - expect(getNewsItemsFromApiResponse(null)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are undefined', () => { - expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are null', () => { - expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); - }); - - test('it returns the expected news items when the browser language matches the i18n values in the response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { - const allCapsLang = 'EN'; - - document.documentElement.lang = allCapsLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { - const nonMatchingLang = 'ja'; - - document.documentElement.lang = nonMatchingLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language is an empty string', () => { - const emptyLang = ''; - - document.documentElement.lang = emptyLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news item when parsing a raw JSON response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); - }); - - describe('translated items', () => { - const translatedDescription = - 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; - const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; - const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; - const translatedTitle = 'SIEMに関する質問はありますか?'; - - const withNonDefaultTranslations: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?', ja: translatedTitle }, - description: { - en: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - ja: translatedDescription, - }, - link_text: null, - link_url: { - en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - ja: translatedLinkUrl, - }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - ja: translatedImageUrl, - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - test('it returns a translated description when the browser language matches additional translated content', () => { - const lang = 'ja'; // an additional translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( - translatedDescription - ); - }); - - test('it returns a translated imageUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( - translatedImageUrl - ); - }); - - test('it returns a translated linkUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( - translatedLinkUrl - ); - }); - - test('it returns a translated title when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - translatedTitle - ); - }); - - test('it returns the default translated title when the browser language matches additional translated content', () => { - const lang = 'fr'; // no translation for this language - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - - test('it returns the default translated title when the browser language is an empty string', () => { - const lang = ''; // just an empty string - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - }); - - test('it generates a news item hash when an item does NOT include it', () => { - const lang = 'en'; - - const itemHasNoHash: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?' }, - description: { - en: 'some description', - }, - link_text: null, - link_url: { en: 'https://example.com/link-url' }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: 'https://example.com/image-url', - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); - }); - }); - - describe('fetchNews', () => { - const mockKibanaServices = KibanaServices.get as jest.Mock; - const fetchMock = jest.fn(); - mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rawNewsApiResponse); - }); - - test('it returns the raw API response from the news feed', async () => { - const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; - expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); - }); - }); - - describe('showNewsItem', () => { - const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z - - let dateNowSpy: { mockRestore: () => void }; - - beforeAll(() => { - dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); - }); - - afterAll(() => { - dateNowSpy.mockRestore(); - }); - - test('it should return true when the article has already been published, and will expire in the future', () => { - const alreadyPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Show this post', - }; - - expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); - }); - - test('it should return false when the article was published exactly "now", and will expire in the future', () => { - const publishedJustNowAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article has not been published yet, and has not expired yet', () => { - const notPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 5000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW + 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and will expire exactly now', () => { - const alreadyPublishedAndExpiredNow: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and it already expired', () => { - const articleJustExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW - 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 5000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(articleJustExpired)).toEqual(false); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx deleted file mode 100644 index 6a5e08b287f96..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect, useState } from 'react'; -import chrome from 'ui/chrome'; - -import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers'; -import { useKibana, useUiSetting$ } from '../../lib/kibana'; -import { NewsFeed } from './news_feed'; -import { NewsItem } from './types'; - -export const StatefulNewsFeed = React.memo<{ - enableNewsFeedSetting: string; - newsFeedSetting: string; -}>(({ enableNewsFeedSetting, newsFeedSetting }) => { - const kibanaNewsfeedEnabled = useKibana().services.newsfeed; - const [enableNewsFeed] = useUiSetting$<boolean>(enableNewsFeedSetting); - const [newsFeedUrlSetting] = useUiSetting$<string>(newsFeedSetting); - const [news, setNews] = useState<NewsItem[] | null>(null); - - // respect kibana's global newsfeed.enabled setting - const newsfeedEnabled = kibanaNewsfeedEnabled && enableNewsFeed; - - const newsFeedUrl = getNewsFeedUrl({ - newsFeedUrlSetting, - getKibanaVersion: chrome.getKibanaVersion, - }); - - useEffect(() => { - let canceled = false; - - const fetchData = async () => { - try { - const apiResponse = await fetchNews({ newsFeedUrl }); - - if (!canceled) { - setNews(getNewsItemsFromApiResponse(apiResponse)); - } - } catch { - if (!canceled) { - setNews([]); - } - } - }; - - if (newsfeedEnabled) { - fetchData(); - } - - return () => { - canceled = true; - }; - }, [newsfeedEnabled, newsFeedUrl]); - - return <>{newsfeedEnabled ? <NewsFeed news={news} /> : null}</>; -}); - -StatefulNewsFeed.displayName = 'StatefulNewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx deleted file mode 100644 index 946c4b3a612dd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useCallback } from 'react'; -import { DeleteTimelines } from '../types'; - -import { TimelineDownloader } from './export_timeline'; -import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; -import { exportSelectedTimeline } from '../../../containers/timeline/all/api'; - -export interface ExportTimeline { - disableExportTimelineDownloader: () => void; - enableExportTimelineDownloader: () => void; - isEnableDownloader: boolean; -} - -export const useExportTimeline = (): ExportTimeline => { - const [isEnableDownloader, setIsEnableDownloader] = useState(false); - - const enableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(true); - }, []); - - const disableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(false); - }, []); - - return { - disableExportTimelineDownloader, - enableExportTimelineDownloader, - isEnableDownloader, - }; -}; - -const EditTimelineActionsComponent: React.FC<{ - deleteTimelines: DeleteTimelines | undefined; - ids: string[]; - isEnableDownloader: boolean; - isDeleteTimelineModalOpen: boolean; - onComplete: () => void; - title: string; -}> = ({ - deleteTimelines, - ids, - isEnableDownloader, - isDeleteTimelineModalOpen, - onComplete, - title, -}) => ( - <> - <TimelineDownloader - exportedIds={ids} - getExportedData={exportSelectedTimeline} - isEnableDownloader={isEnableDownloader} - onComplete={onComplete} - /> - {deleteTimelines != null && ( - <DeleteTimelineModalOverlay - deleteTimelines={deleteTimelines} - isModalOpen={isDeleteTimelineModalOpen} - onComplete={onComplete} - savedObjectIds={ids} - title={title} - /> - )} - </> -); - -export const EditTimelineActions = React.memo(EditTimelineActionsComponent); -export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx deleted file mode 100644 index 04f0abe0d00d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx +++ /dev/null @@ -1,628 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../lib/helpers'; -import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; -import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; - -import { StatefulOpenTimeline } from '.'; -import { NotePreviews } from './note_previews'; -import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - -jest.mock('../../lib/kibana'); - -describe('StatefulOpenTimeline', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const title = 'All Timelines / Open Timelines'; - - test('it has the expected initial state', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - const componentProps = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .props(); - - expect(componentProps).toEqual({ - ...componentProps, - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - query: '', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', - }); - }); - - describe('#onQueryChange', () => { - test('it updates the query state with the expected trimmed value when the user enters a query', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - expect( - wrapper - .find('[data-test-subj="search-row"]') - .first() - .prop('query') - ).toEqual('abcd'); - }); - - test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines with'); - }); - - test('echos (renders) the query when the user enters a query', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="selectable-query-text"]') - .first() - .text() - ).toEqual('with "abcd"'); - }); - }); - - describe('#focusInput', () => { - test('focuses the input when the component mounts', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - expect( - wrapper - .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) - .first() - .getDOMNode().id === document.activeElement!.id - ).toBe(true); - }); - }); - - describe('#onAddTimelinesToFavorites', () => { - // This functionality is hiding for now and waiting to see the light in the near future - test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { - const addTimelinesToFavorites = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="favorite-selected"]') - .first() - .simulate('click'); - - expect(addTimelinesToFavorites).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onDeleteSelected', () => { - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { - const deleteTimelines = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="delete-selected"]') - .first() - .simulate('click'); - - expect(deleteTimelines).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onSelectionChange', () => { - test('it updates the selection state when timelines are selected', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - const selectedItems: [] = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - - expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query - }); - }); - - describe('#onTableChange', () => { - test('it updates the sort state when the user clicks on a column to sort it', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('desc'); - - wrapper - .find('thead tr th button') - .at(0) - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('asc'); - }); - }); - - describe('#onToggleOnlyFavorites', () => { - test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(false); - - wrapper - .find('[data-test-subj="only-favorites-toggle"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(true); - }); - }); - - describe('#onToggleShowNotes', () => { - test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({}); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({ - '10849df0-7b44-11e9-a608-ab3d811609': ( - <NotePreviews - notes={ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes != null - ? mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes.map( - note => ({ ...note, savedObjectId: note.noteId }) - ) - : [] - } - /> - ), - }); - }); - - test('it renders the expanded notes when the expand button is clicked', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="note-previews-container"]') - .find('[data-test-subj="updated-by"]') - .first() - .text() - ).toEqual('elastic'); - }); - }); - - test('it renders the title', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - expect( - wrapper - .find('[data-test-subj="header-section-title"]') - .first() - .text() - ).toEqual(title); - }); - - describe('#resetSelectionState', () => { - test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - const getSelectedItem = (): [] => - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - await wait(); - expect(getSelectedItem().length).toEqual(0); - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - expect(getSelectedItem().length).toEqual(13); - }); - }); - - test('it renders the expected count of matching timelines when no query has been entered', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <TestProviderWithoutDragAndDrop> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </TestProviderWithoutDragAndDrop> - </MockedProvider> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines '); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find( - `[data-test-subj="title-${ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId - }"]` - ) - .first() - .simulate('click'); - - expect(onOpenTimeline).toHaveBeenCalledWith({ - duplicate: false, - timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] - .savedObjectId, - }); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="open-duplicate"]') - .first() - .simulate('click'); - - expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx deleted file mode 100644 index c27a6039da29d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ApolloClient from 'apollo-client'; -import React, { useEffect, useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { Dispatch } from 'redux'; -import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; -import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; -import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; -import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query'; -import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; -import { State, timelineSelectors } from '../../store'; -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { - createTimeline as dispatchCreateNewTimeline, - updateIsLoading as dispatchUpdateIsLoading, -} from '../../store/timeline/actions'; -import { OpenTimeline } from './open_timeline'; -import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; -import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; -import { - ActionTimelineToShow, - DeleteTimelines, - EuiSearchBarQuery, - OnDeleteSelected, - OnOpenTimeline, - OnQueryChange, - OnSelectionChange, - OnTableChange, - OnTableChangeParams, - OpenTimelineProps, - OnToggleOnlyFavorites, - OpenTimelineResult, - OnToggleShowNotes, - OnDeleteOneTimeline, -} from './types'; -import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; - -interface OwnProps<TCache = object> { - apolloClient: ApolloClient<TCache>; - /** Displays open timeline in modal */ - isModal: boolean; - closeModalTimeline?: () => void; - hideActions?: ActionTimelineToShow[]; - onOpenTimeline?: (timeline: TimelineModel) => void; -} - -export type OpenTimelineOwnProps = OwnProps & - Pick< - OpenTimelineProps, - 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' - > & - PropsFromRedux; - -/** Returns a collection of selected timeline ids */ -export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => - selectedItems.reduce<string[]>( - (validSelections, timelineResult) => - timelineResult.savedObjectId != null - ? [...validSelections, timelineResult.savedObjectId] - : validSelections, - [] - ); - -/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ -export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( - ({ - apolloClient, - closeModalTimeline, - createNewTimeline, - defaultPageSize, - hideActions = [], - isModal = false, - importDataModalToggle, - onOpenTimeline, - setImportDataModalToggle, - timeline, - title, - updateTimeline, - updateIsLoading, - }) => { - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< - Record<string, JSX.Element> - >({}); - /** Only query for favorite timelines when true */ - const [onlyFavorites, setOnlyFavorites] = useState(false); - /** The requested page of results */ - const [pageIndex, setPageIndex] = useState(0); - /** The requested size of each page of search results */ - const [pageSize, setPageSize] = useState(defaultPageSize); - /** The current search criteria */ - const [search, setSearch] = useState(''); - /** The currently-selected timelines in the table */ - const [selectedItems, setSelectedItems] = useState<OpenTimelineResult[]>([]); - /** The requested sort direction of the query results */ - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); - /** The requested field to sort on */ - const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - - /** Invoked when the user presses enters to submit the text in the search input */ - const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { - setSearch(query.queryText.trim()); - }, []); - - /** Focuses the input that filters the field browser */ - const focusInput = () => { - const elements = document.querySelector<HTMLElement>(`.${OPEN_TIMELINE_CLASS_NAME} input`); - - if (elements != null) { - elements.focus(); - } - }; - - /* This feature will be implemented in the near future, so we are keeping it to know what to do */ - - /** Invoked when the user clicks the action to add the selected timelines to favorites */ - // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { - // const { addTimelinesToFavorites } = this.props; - // const { selectedItems } = this.state; - // if (addTimelinesToFavorites != null) { - // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); - // TODO: it's not possible to clear the selection state of the newly-favorited - // items, because we can't pass the selection state as props to the table. - // See: https://github.com/elastic/eui/issues/1077 - // TODO: the query must re-execute to show the results of the mutation - // } - // }; - - const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( - (timelineIds: string[]) => { - deleteTimelines(timelineIds, { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - }, - [search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites] - ); - - /** Invoked when the user clicks the action to delete the selected timelines */ - const onDeleteSelected: OnDeleteSelected = useCallback(() => { - deleteTimelines(getSelectedTimelineIds(selectedItems), { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - - // NOTE: we clear the selection state below, but if the server fails to - // delete a timeline, it will remain selected in the table: - resetSelectionState(); - - // TODO: the query must re-execute to show the results of the deletion - }, [selectedItems, search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]); - - /** Invoked when the user selects (or de-selects) timelines */ - const onSelectionChange: OnSelectionChange = useCallback( - (newSelectedItems: OpenTimelineResult[]) => { - setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 - }, - [] - ); - - /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ - const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { - const { index, size } = page; - const { field, direction } = sort; - setPageIndex(index); - setPageSize(size); - setSortDirection(direction); - setSortField(field); - }, []); - - /** Invoked when the user toggles the option to only view favorite timelines */ - const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - const onToggleShowNotes: OnToggleShowNotes = useCallback( - (newItemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => { - setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); - }, - [] - ); - - /** Resets the selection state such that all timelines are unselected */ - const resetSelectionState = useCallback(() => { - setSelectedItems([]); - }, []); - - const openTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - if (isModal && closeModalTimeline != null) { - closeModalTimeline(); - } - - queryTimelineById({ - apolloClient, - duplicate, - onOpenTimeline, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const deleteTimelines: DeleteTimelines = useCallback( - (timelineIds: string[], variables?: AllTimelinesVariables) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - apolloClient.mutate<DeleteTimelineMutation.Mutation, DeleteTimelineMutation.Variables>({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - refetchQueries: [ - { - query: allTimelinesQuery, - variables, - }, - ], - }); - }, - [apolloClient, createNewTimeline, timeline] - ); - - useEffect(() => { - focusInput(); - }, []); - - return ( - <AllTimelinesQuery - pageInfo={{ - pageIndex: pageIndex + 1, - pageSize, - }} - search={search} - sort={{ sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }} - onlyUserFavorite={onlyFavorites} - > - {({ timelines, loading, totalCount, refetch }) => { - return !isModal ? ( - <OpenTimeline - data-test-subj={'open-timeline'} - deleteTimelines={onDeleteOneTimeline} - defaultPageSize={defaultPageSize} - isLoading={loading} - itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} - importDataModalToggle={importDataModalToggle} - onAddTimelinesToFavorites={undefined} - onDeleteSelected={onDeleteSelected} - onlyFavorites={onlyFavorites} - onOpenTimeline={openTimeline} - onQueryChange={onQueryChange} - onSelectionChange={onSelectionChange} - onTableChange={onTableChange} - onToggleOnlyFavorites={onToggleOnlyFavorites} - onToggleShowNotes={onToggleShowNotes} - pageIndex={pageIndex} - pageSize={pageSize} - query={search} - refetch={refetch} - searchResults={timelines} - setImportDataModalToggle={setImportDataModalToggle} - selectedItems={selectedItems} - sortDirection={sortDirection} - sortField={sortField} - title={title} - totalSearchResultsCount={totalCount} - /> - ) : ( - <OpenTimelineModalBody - data-test-subj={'open-timeline-modal'} - deleteTimelines={onDeleteOneTimeline} - defaultPageSize={defaultPageSize} - hideActions={hideActions} - isLoading={loading} - itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} - onAddTimelinesToFavorites={undefined} - onlyFavorites={onlyFavorites} - onOpenTimeline={openTimeline} - onQueryChange={onQueryChange} - onSelectionChange={onSelectionChange} - onTableChange={onTableChange} - onToggleOnlyFavorites={onToggleOnlyFavorites} - onToggleShowNotes={onToggleShowNotes} - pageIndex={pageIndex} - pageSize={pageSize} - query={search} - searchResults={timelines} - selectedItems={selectedItems} - sortDirection={sortDirection} - sortField={sortField} - title={title} - totalSearchResultsCount={totalCount} - /> - ); - }} - </AllTimelinesQuery> - ); - } -); - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; - - return { - timeline, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - createNewTimeline: ({ - id, - columns, - show, - }: { - id: string; - columns: ColumnHeaderOptions[]; - show?: boolean; - }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx deleted file mode 100644 index ca8fa50c572fe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../../lib/helpers'; -import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; - -import { OpenTimelineModal } from '.'; - -jest.mock('../../../lib/kibana'); -jest.mock('../../../utils/apollo_context', () => ({ - useApolloClient: () => ({}), -})); - -describe('OpenTimelineModal', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - test('it renders the expected modal', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <OpenTimelineModal onClose={jest.fn()} /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts deleted file mode 100644 index b7cc92ebd183f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SetStateAction, Dispatch } from 'react'; -import { AllTimelinesVariables } from '../../containers/timeline/all'; -import { TimelineModel } from '../../store/timeline/model'; -import { NoteResult } from '../../graphql/types'; -import { Refetch } from '../../store/inputs/model'; - -/** The users who added a timeline to favorites */ -export interface FavoriteTimelineResult { - userId?: number | null; - userName?: string | null; - favoriteDate?: number | null; -} - -export interface TimelineResultNote { - savedObjectId?: string | null; - note?: string | null; - noteId?: string | null; - updated?: number | null; - updatedBy?: string | null; -} - -export interface TimelineActionsOverflowColumns { - width: string; - actions: Array<{ - name: string; - icon?: string; - onClick?: (timeline: OpenTimelineResult) => void; - description: string; - render?: (timeline: OpenTimelineResult) => JSX.Element; - } | null>; -} - -/** The results of the query run by the OpenTimeline component */ -export interface OpenTimelineResult { - created?: number | null; - description?: string | null; - eventIdToNoteIds?: Readonly<Record<string, string[]>> | null; - favorite?: FavoriteTimelineResult[] | null; - noteIds?: string[] | null; - notes?: TimelineResultNote[] | null; - pinnedEventIds?: Readonly<Record<string, boolean>> | null; - savedObjectId?: string | null; - title?: string | null; - updated?: number | null; - updatedBy?: string | null; -} - -/** - * EuiSearchBar returns this object when the user changes the query. At the - * time of this writing, there is no typescript definition for this type, so - * only the properties used by the Open Timeline component are exposed. - */ -export interface EuiSearchBarQuery { - queryText: string; -} - -/** Performs IO to delete the specified timelines */ -export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; - -/** Invoked when the user clicks the action make the selected timelines favorites */ -export type OnAddTimelinesToFavorites = () => void; - -/** Invoked when the user clicks the action to delete the selected timelines */ -export type OnDeleteSelected = () => void; -export type OnDeleteOneTimeline = (timelineIds: string[]) => void; - -/** Invoked when the user clicks on the name of a timeline to open it */ -export type OnOpenTimeline = ({ - duplicate, - timelineId, -}: { - duplicate: boolean; - timelineId: string; -}) => void; - -export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; -export type SetActionTimeline = Dispatch<SetStateAction<OpenTimelineResult | undefined>>; -export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; -/** Invoked when the user presses enters to submit the text in the search input */ -export type OnQueryChange = (query: EuiSearchBarQuery) => void; - -/** Invoked when the user selects (or de-selects) timelines in the table */ -export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; - -/** Invoked when the user toggles the option to only view favorite timelines */ -export type OnToggleOnlyFavorites = () => void; - -/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ -export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => void; - -/** Parameters to the OnTableChange callback */ -export interface OnTableChangeParams { - page: { - index: number; - size: number; - }; - sort: { - field: string; - direction: 'asc' | 'desc'; - }; -} - -/** Invoked by the EUI table implementation when the user interacts with the table */ -export type OnTableChange = (tableChange: OnTableChangeParams) => void; - -export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; - -export interface OpenTimelineProps { - /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ - deleteTimelines?: DeleteTimelines; - /** The default requested size of each page of search results */ - defaultPageSize: number; - /** Displays an indicator that data is loading when true */ - isLoading: boolean; - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - itemIdToExpandedNotesRowMap: Record<string, JSX.Element>; - /** Display import timelines modal*/ - importDataModalToggle?: boolean; - /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; - /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onDeleteSelected?: OnDeleteSelected; - /** Only show favorite timelines when true */ - onlyFavorites: boolean; - /** Invoked when the user presses enter after typing in the search bar */ - onQueryChange: OnQueryChange; - /** Invoked when the user selects (or de-selects) timelines in the table */ - onSelectionChange: OnSelectionChange; - /** Invoked when the user clicks on the name of a timeline to open it */ - onOpenTimeline: OnOpenTimeline; - /** Invoked by the EUI table implementation when the user interacts with the table */ - onTableChange: OnTableChange; - /** Invoked when the user toggles the option to only show favorite timelines */ - onToggleOnlyFavorites: OnToggleOnlyFavorites; - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - onToggleShowNotes: OnToggleShowNotes; - /** the requested page of results */ - pageIndex: number; - /** the requested size of each page of search results */ - pageSize: number; - /** The currently applied search criteria */ - query: string; - /** Refetch table */ - refetch?: Refetch; - /** The results of executing a search */ - searchResults: OpenTimelineResult[]; - /** the currently-selected timelines in the table */ - selectedItems: OpenTimelineResult[]; - /** Toggle export timelines modal*/ - setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>; - /** the requested sort direction of the query results */ - sortDirection: 'asc' | 'desc'; - /** the requested field to sort on */ - sortField: string; - /** The title of the Open Timeline component */ - title: string; - /** The total (server-side) count of the search results */ - totalSearchResultsCount: number; - /** Hide action on timeline if needed it */ - hideActions?: ActionTimelineToShow[]; -} - -export interface UpdateTimeline { - duplicate: boolean; - id: string; - from: number; - notes: NoteResult[] | null | undefined; - timeline: TimelineModel; - to: number; - ruleNote?: string; -} - -export type DispatchUpdateTimeline = ({ - duplicate, - id, - from, - notes, - timeline, - to, - ruleNote, -}: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts deleted file mode 100644 index d88bc2bf3b7e6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Filter } from '../../../../../../../../src/plugins/data/public'; - -export const createFilter = ( - key: string, - value: string[] | string | null | undefined, - negate: boolean = false -): Filter => { - const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; - return queryValue != null - ? { - meta: { - alias: null, - negate, - disabled: false, - type: 'phrase', - key, - value: queryValue, - params: { - query: queryValue, - }, - }, - query: { - match: { - [key]: { - query: queryValue, - type: 'phrase', - }, - }, - }, - } - : ({ - exists: { - field: key, - }, - meta: { - alias: null, - disabled: false, - key, - negate: value === undefined, - type: 'exists', - value: 'exists', - }, - } as Filter); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx deleted file mode 100644 index 127eb3bae0284..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { WithHoverActions } from '../../with_hover_actions'; -import { useKibana } from '../../../lib/kibana'; - -import * as i18n from './translations'; - -export * from './helpers'; - -interface OwnProps { - children: JSX.Element; - filter: Filter; - onFilterAdded?: () => void; -} - -export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( - ({ children, filter, onFilterAdded }) => { - const { filterManager } = useKibana().services.data.query; - - const filterForValue = useCallback(() => { - filterManager.addFilters(filter); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - const filterOutValue = useCallback(() => { - filterManager.addFilters({ - ...filter, - meta: { - ...filter.meta, - negate: true, - }, - }); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - return ( - <WithHoverActions - hoverContent={ - <div data-test-subj="hover-actions-container"> - <EuiToolTip content={i18n.FILTER_FOR_VALUE}> - <EuiButtonIcon - aria-label={i18n.FILTER_FOR_VALUE} - color="text" - data-test-subj="add-to-filter" - iconType="magnifyWithPlus" - onClick={filterForValue} - /> - </EuiToolTip> - - <EuiToolTip content={i18n.FILTER_OUT_VALUE}> - <EuiButtonIcon - aria-label={i18n.FILTER_OUT_VALUE} - color="text" - data-test-subj="filter-out-value" - iconType="magnifyWithMinus" - onClick={filterOutValue} - /> - </EuiToolTip> - </div> - } - render={() => children} - /> - ); - } -); - -AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx deleted file mode 100644 index a0ca5f855237c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { getOr } from 'lodash/fp'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; -import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getEmptyTagValue } from '../../../empty_value'; -import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; -import { HostItem } from '../../../../graphql/types'; -import { Loader } from '../../../loader'; -import { IPDetailsLink } from '../../../links'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host'; - -import * as i18n from './translations'; - -interface HostSummaryProps { - data: HostItem; - id: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - narrowDateRange: NarrowDateRange; -} - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( - <EuiFlexItem key={key}> - <DescriptionListStyled listItems={descriptionList} /> - </EuiFlexItem> -); - -export const HostOverview = React.memo<HostSummaryProps>( - ({ - data, - loading, - id, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); - - const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( - <DefaultFieldRenderer - rowItems={getOr([], fieldName, fieldData)} - attrName={fieldName} - idPrefix="host-overview" - /> - ); - - const column: DescriptionList[] = [ - { - title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), - }, - { - title: i18n.FIRST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - <FirstLastSeenHost - hostname={data.host.name[0]} - type={FirstLastSeenHostType.FIRST_SEEN} - /> - ) : ( - getEmptyTagValue() - ), - }, - { - title: i18n.LAST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - <FirstLastSeenHost - hostname={data.host.name[0]} - type={FirstLastSeenHostType.LAST_SEEN} - /> - ) : ( - getEmptyTagValue() - ), - }, - ]; - const firstColumn = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - <AnomalyScores - anomalies={anomaliesData} - startDate={startDate} - endDate={endDate} - isLoading={isLoadingAnomaliesData} - narrowDateRange={narrowDateRange} - /> - ), - }, - ] - : column; - - const descriptionLists: Readonly<DescriptionList[][]> = [ - firstColumn, - [ - { - title: i18n.IP_ADDRESSES, - description: ( - <DefaultFieldRenderer - rowItems={getOr([], 'host.ip', data)} - attrName={'host.ip'} - idPrefix="host-overview" - render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())} - /> - ), - }, - { - title: i18n.MAC_ADDRESSES, - description: getDefaultRenderer('host.mac', data), - }, - { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, - ], - [ - { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, - { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, - { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, - { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, - ], - [ - { - title: i18n.CLOUD_PROVIDER, - description: getDefaultRenderer('cloud.provider', data), - }, - { - title: i18n.REGION, - description: getDefaultRenderer('cloud.region', data), - }, - { - title: i18n.INSTANCE_ID, - description: getDefaultRenderer('cloud.instance.id', data), - }, - { - title: i18n.MACHINE_TYPE, - description: getDefaultRenderer('cloud.machine.type', data), - }, - ], - ]; - - return ( - <InspectButtonContainer> - <OverviewWrapper> - <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - <Loader - overlay - overlayBackground={ - darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor - } - size="xl" - /> - )} - </OverviewWrapper> - </InspectButtonContainer> - ); - } -); - -HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx deleted file mode 100644 index 44dc75cd6bad3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; -import styled, { createGlobalStyle } from 'styled-components'; - -/* - SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly - and `EuiPopover`, `EuiToolTip` global styles -*/ -export const AppGlobalStyle = createGlobalStyle` - /* dirty hack to fix draggables with tooltip on FF */ - body#siem-app { - position: static; - } - /* end of dirty hack to fix draggables with tooltip on FF */ - - div.app-wrapper { - background-color: rgba(0,0,0,0); - } - - div.application { - background-color: rgba(0,0,0,0); - } - - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - min-width: 24px; - } - .euiToolTip { - z-index: 9950 !important; - } - - /* - overrides the default styling of euiComboBoxOptionsList because it's implemented - as a popover, so it's not selectable as a child of the styled component - */ - .euiComboBoxOptionsList { - z-index: 9999; - } - - /* overrides default styling in angular code that was not theme-friendly */ - .euiPanel-loading-hide-border { - border: none; - } - - /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ - body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { - visibility: hidden !important; - } - - /* ensure elastic charts tooltips appear above open euiPopovers */ - .echTooltip { - z-index: 9950; - } - -`; - -export const DescriptionListStyled = styled(EuiDescriptionList)` - ${({ theme }) => ` - dt { - font-size: ${theme.eui.euiFontSizeXS} !important; - } - dd { - width: fit-content; - } - dd > div { - width: fit-content; - } - `} -`; - -DescriptionListStyled.displayName = 'DescriptionListStyled'; - -export const PageContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - height: 100%; - padding: 1rem; - overflow: hidden; - margin: 0px; -`; - -PageContainer.displayName = 'PageContainer'; - -export const PageContent = styled.div` - flex: 1 1 auto; - height: 100%; - position: relative; - overflow-y: hidden; - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - margin-top: 62px; -`; - -PageContent.displayName = 'PageContent'; - -export const FlexPage = styled(EuiPage)` - flex: 1 0 0; -`; - -FlexPage.displayName = 'FlexPage'; - -export const PageHeader = styled.div` - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - display: flex; - user-select: none; - padding: 1rem 1rem 0rem 1rem; - width: 100vw; - position: fixed; -`; - -PageHeader.displayName = 'PageHeader'; - -export const FooterContainer = styled.div` - flex: 0; - bottom: 0; - color: #666; - left: 0; - position: fixed; - text-align: left; - user-select: none; - width: 100%; - background-color: #f5f7fa; - padding: 16px; - border-top: 1px solid #d3dae6; -`; - -FooterContainer.displayName = 'FooterContainer'; - -export const PaneScrollContainer = styled.div` - height: 100%; - overflow-y: scroll; - > div:last-child { - margin-bottom: 3rem; - } -`; - -PaneScrollContainer.displayName = 'PaneScrollContainer'; - -export const Pane = styled.div` - height: 100%; - overflow: hidden; - user-select: none; -`; - -Pane.displayName = 'Pane'; - -export const PaneHeader = styled.div` - display: flex; -`; - -PaneHeader.displayName = 'PaneHeader'; - -export const Pane1FlexContent = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - height: 100%; -`; - -Pane1FlexContent.displayName = 'Pane1FlexContent'; - -export const CountBadge = styled(EuiBadge)` - margin-left: 5px; -` 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 a652fef5508fc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; -import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; -import { networkModel } from '../../../../store'; -import { getEmptyTagValue } from '../../../empty_value'; - -import { - autonomousSystemRenderer, - dateRenderer, - hostIdRenderer, - hostNameRenderer, - locationRenderer, - reputationRenderer, - whoisRenderer, -} from '../../../field_renderers/field_renderers'; -import * as i18n from './translations'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { Loader } from '../../../loader'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; - -interface OwnProps { - data: IpOverviewData; - flowTarget: FlowTarget; - id: string; - ip: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - type: networkModel.NetworkType; - narrowDateRange: NarrowDateRange; -} - -export type IpOverviewProps = OwnProps; - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { - return ( - <EuiFlexItem key={key}> - <DescriptionListStyled listItems={descriptionList} /> - </EuiFlexItem> - ); -}; - -export const IpOverview = React.memo<IpOverviewProps>( - ({ - id, - ip, - data, - loading, - flowTarget, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); - const typeData: Overview = data[flowTarget]!; - const column: DescriptionList[] = [ - { - title: i18n.LOCATION, - description: locationRenderer( - [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], - data - ), - }, - { - title: i18n.AUTONOMOUS_SYSTEM, - description: typeData - ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) - : getEmptyTagValue(), - }, - ]; - - const firstColumn: DescriptionList[] = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - <AnomalyScores - anomalies={anomaliesData} - startDate={startDate} - endDate={endDate} - isLoading={isLoadingAnomaliesData} - narrowDateRange={narrowDateRange} - /> - ), - }, - ] - : column; - - const descriptionLists: Readonly<DescriptionList[][]> = [ - firstColumn, - [ - { - title: i18n.FIRST_SEEN, - description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), - }, - { - title: i18n.LAST_SEEN, - description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), - }, - ], - [ - { - title: i18n.HOST_ID, - description: typeData - ? hostIdRenderer({ host: data.host, ipFilter: ip }) - : getEmptyTagValue(), - }, - { - title: i18n.HOST_NAME, - description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), - }, - ], - [ - { title: i18n.WHOIS, description: whoisRenderer(ip) }, - { title: i18n.REPUTATION, description: reputationRenderer(ip) }, - ], - ]; - - return ( - <InspectButtonContainer> - <OverviewWrapper> - <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - <Loader - overlay - overlayBackground={ - darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor - } - size="xl" - /> - )} - </OverviewWrapper> - </InspectButtonContainer> - ); - } -); - -IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx deleted file mode 100644 index b43efbbde51b3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; -import { - ID as OverviewHostQueryId, - OverviewHostQuery, -} from '../../../../containers/overview/overview_host'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getHostsUrl } from '../../../link_to'; -import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; -import { manageQuery } from '../../../page/manage_query'; -import { inputsModel } from '../../../../store/inputs'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OwnProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewHostStatsManage = manageQuery(OverviewHostStats); -export type OverviewHostProps = OwnProps; - -const OverviewHostComponent: React.FC<OverviewHostProps> = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - const hostPageButton = useMemo( - () => ( - <EuiButton href={getHostsUrl(urlSearch)}> - <FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" /> - </EuiButton> - ), - [urlSearch] - ); - return ( - <EuiFlexItem> - <InspectButtonContainer> - <EuiPanel> - <OverviewHostQuery - data-test-subj="overview-host-query" - endDate={endDate} - filterQuery={filterQuery} - sourceId="default" - startDate={startDate} - > - {({ overviewHost, loading, id, inspect, refetch }) => { - const hostEventsCount = getOverviewHostStats(overviewHost).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); - - return ( - <> - <HeaderSection - id={OverviewHostQueryId} - subtitle={ - !isEmpty(overviewHost) ? ( - <FormattedMessage - defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}" - id="xpack.siem.overview.overviewHost.hostsSubtitle" - values={{ - formattedHostEventsCount, - hostEventsCount, - }} - /> - ) : ( - <>{''}</> - ) - } - title={ - <FormattedMessage - id="xpack.siem.overview.hostsTitle" - defaultMessage="Host events" - /> - } - > - {hostPageButton} - </HeaderSection> - - <OverviewHostStatsManage - loading={loading} - data={overviewHost} - setQuery={setQuery} - id={id} - inspect={inspect} - refetch={refetch} - /> - </> - ); - }} - </OverviewHostQuery> - </EuiPanel> - </InspectButtonContainer> - </EuiFlexItem> - ); -}; - -export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx deleted file mode 100644 index af50fa88e5fe8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { manageQuery } from '../../../page/manage_query'; -import { - ID as OverviewNetworkQueryId, - OverviewNetworkQuery, -} from '../../../../containers/overview/overview_network'; -import { inputsModel } from '../../../../store/inputs'; -import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; -import { getNetworkUrl } from '../../../link_to'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OverviewNetworkProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); - -const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.network); - const networkPageButton = useMemo( - () => ( - <EuiButton href={getNetworkUrl(urlSearch)}> - <FormattedMessage id="xpack.siem.overview.networkAction" defaultMessage="View network" /> - </EuiButton> - ), - [urlSearch] - ); - return ( - <EuiFlexItem> - <InspectButtonContainer> - <EuiPanel> - <OverviewNetworkQuery - data-test-subj="overview-network-query" - endDate={endDate} - filterQuery={filterQuery} - sourceId="default" - startDate={startDate} - > - {({ overviewNetwork, loading, id, inspect, refetch }) => { - const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedNetworkEventsCount = numeral(networkEventsCount).format( - defaultNumberFormat - ); - - return ( - <> - <HeaderSection - id={OverviewNetworkQueryId} - subtitle={ - !isEmpty(overviewNetwork) ? ( - <FormattedMessage - defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}" - id="xpack.siem.overview.overviewNetwork.networkSubtitle" - values={{ - formattedNetworkEventsCount, - networkEventsCount, - }} - /> - ) : ( - <>{''}</> - ) - } - title={ - <FormattedMessage - id="xpack.siem.overview.networkTitle" - defaultMessage="Network events" - /> - } - > - {networkPageButton} - </HeaderSection> - - <OverviewNetworkStatsManage - loading={loading} - data={overviewNetwork} - id={id} - inspect={inspect} - setQuery={setQuery} - refetch={refetch} - /> - </> - ); - }} - </OverviewNetworkQuery> - </EuiPanel> - </InspectButtonContainer> - </EuiFlexItem> - ); -}; - -OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; - -export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx deleted file mode 100644 index 2f743c3387209..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; -import { Direction } from '../../graphql/types'; - -import { BasicTableProps, PaginatedTable } from './index'; -import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -jest.mock('react', () => { - const r = jest.requireActual('react'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { ...r, memo: (x: any) => x }; -}); - -describe('Paginated Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - let loadPage: jest.Mock<number>; - let updateLimitPagination: jest.Mock<number>; - let updateActivePage: jest.Mock<number>; - beforeEach(() => { - loadPage = jest.fn(); - updateLimitPagination = jest.fn(); - updateActivePage = jest.fn(); - }); - - describe('rendering', () => { - test('it renders the default load more table', () => { - const wrapper = shallow( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={-1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={[]} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect( - wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() - ).toBeTruthy(); - }); - - test('it renders the over loading panel after data has been in the table ', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); - }); - - test('it renders the correct amount of pages and starts at activePage: 0', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - const paginiationProps = wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .props(); - - const expectedPaginationProps = { - 'data-test-subj': 'numberedPagination', - pageCount: 10, - activePage: 0, - }; - expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); - }); - - test('it render popover to select new limit in table', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); - }); - - test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - - test('It should render a sort icon if sorting is defined', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={sortedHosts} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); - }); - - test('Should display toast when user reaches end of results max', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls.length).toEqual(0); - }); - - test('Should show items per row if totalCount is greater than items', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={30} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); - }); - - test('Should hide items per row if totalCount is less than items', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={1} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - }); - - describe('Events', () => { - test('should call updateActivePage with 1 when clicking to the first page', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[0][0]).toEqual(1); - }); - - test('Should call updateActivePage with 0 when you pick a new limit', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[1][0]).toEqual(0); - }); - - test('should update the page when the activePage is changed from redux', () => { - const ourProps: BasicTableProps<unknown> = { - activePage: 3, - columns: getHostsColumns(), - headerCount: 1, - headerSupplement: <p>{'My test supplement.'}</p>, - headerTitle: 'Hosts', - headerTooltip: 'My test tooltip', - headerUnit: 'Test Unit', - itemsPerRow: rowItems, - limit: 1, - loading: false, - loadPage, - pageOfItems: mockData.Hosts.edges, - showMorePagesIndicator: true, - totalCount: 10, - updateActivePage, - updateLimitPagination: limit => updateLimitPagination({ limit }), - }; - - // enzyme does not allow us to pass props to child of HOC - // so we make a component to pass it the props context - // ComponentWithContext will pass the changed props to Component - // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ComponentWithContext = (props: BasicTableProps<any>) => { - return ( - <ThemeProvider theme={theme}> - <PaginatedTable {...props} /> - </ThemeProvider> - ); - }; - - const wrapper = mount(<ComponentWithContext {...ourProps} />); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(3); - wrapper.setProps({ activePage: 0 }); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(0); - }); - - test('Should call updateLimitPagination when you pick a new limit', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateLimitPagination).toBeCalled(); - }); - - test('Should call onChange when you choose a new sort in the table', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={sortedHosts} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - expect(mockOnChange).toBeCalled(); - expect(mockOnChange.mock.calls[0]).toEqual([ - { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, - ]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx deleted file mode 100644 index e481fe7245201..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiBasicTable, - EuiBasicTableProps, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiGlobalToastListToast as Toast, - EuiLoadingContent, - EuiPagination, - EuiPopover, - Direction, -} from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; -import styled from 'styled-components'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; -import { AuthTableColumns } from '../page/hosts/authentications_table'; -import { HostsTableColumns } from '../page/hosts/hosts_table'; -import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; -import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; -import { - NetworkTopNFlowColumns, - NetworkTopNFlowColumnsIpDetails, -} from '../page/network/network_top_n_flow_table/columns'; -import { - NetworkTopCountriesColumns, - NetworkTopCountriesColumnsIpDetails, -} from '../page/network/network_top_countries_table/columns'; -import { TlsColumns } from '../page/network/tls_table/columns'; -import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; -import { UsersColumns } from '../page/network/users_table/columns'; -import { HeaderSection } from '../header_section'; -import { Loader } from '../loader'; -import { useStateToaster } from '../toasters'; - -import * as i18n from './translations'; -import { Panel } from '../panel'; -import { InspectButtonContainer } from '../inspect'; - -const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; - -export interface ItemsPerRow { - text: string; - numberOfRow: number; -} - -export interface SortingBasicTable { - field: string; - direction: Direction; - allowNeutralSort?: boolean; -} - -export interface Criteria { - page?: { index: number; size: number }; - sort?: SortingBasicTable; -} - -declare type HostsTableColumnsTest = [ - Columns<string>, - Columns<string>, - Columns<string>, - Columns<string> -]; - -declare type BasicTableColumns = - | AuthTableColumns - | HostsTableColumns - | HostsTableColumnsTest - | NetworkDnsColumns - | NetworkHttpColumns - | NetworkTopCountriesColumns - | NetworkTopCountriesColumnsIpDetails - | NetworkTopNFlowColumns - | NetworkTopNFlowColumnsIpDetails - | TlsColumns - | UncommonProcessTableColumns - | UsersColumns; - -declare type SiemTables = BasicTableProps<BasicTableColumns>; - -// Using telescoping templates to remove 'any' that was polluting downstream column type checks -export interface BasicTableProps<T> { - activePage: number; - columns: T; - dataTestSubj?: string; - headerCount: number; - headerSupplement?: React.ReactElement; - headerTitle: string | React.ReactElement; - headerTooltip?: string; - headerUnit: string | React.ReactElement; - id?: string; - itemsPerRow?: ItemsPerRow[]; - isInspect?: boolean; - limit: number; - loading: boolean; - loadPage: (activePage: number) => void; - onChange?: (criteria: Criteria) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pageOfItems: any[]; - showMorePagesIndicator: boolean; - sorting?: SortingBasicTable; - totalCount: number; - updateActivePage: (activePage: number) => void; - updateLimitPagination: (limit: number) => void; -} -type Func<T> = (arg: T) => string | number; - -export interface Columns<T, U = T> { - align?: string; - field?: string; - hideForMobile?: boolean; - isMobileHeader?: boolean; - name: string | React.ReactNode; - render?: (item: T, node: U) => React.ReactNode; - sortable?: boolean | Func<T>; - truncateText?: boolean; - width?: string; -} - -const PaginatedTableComponent: FC<SiemTables> = ({ - activePage, - columns, - dataTestSubj = DEFAULT_DATA_TEST_SUBJ, - headerCount, - headerSupplement, - headerTitle, - headerTooltip, - headerUnit, - id, - isInspect, - itemsPerRow, - limit, - loading, - loadPage, - onChange = noop, - pageOfItems, - showMorePagesIndicator, - sorting = null, - totalCount, - updateActivePage, - updateLimitPagination, -}) => { - const [myLoading, setMyLoading] = useState(loading); - const [myActivePage, setActivePage] = useState(activePage); - const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); - const [isPopoverOpen, setPopoverOpen] = useState(false); - - const pageCount = Math.ceil(totalCount / limit); - const dispatchToaster = useStateToaster()[1]; - - useEffect(() => { - setActivePage(activePage); - }, [activePage]); - - useEffect(() => { - if (headerCount >= 0 && loadingInitial) { - setLoadingInitial(false); - } - }, [loadingInitial, headerCount]); - - useEffect(() => { - setMyLoading(loading); - }, [loading]); - - const onButtonClick = () => { - setPopoverOpen(!isPopoverOpen); - }; - - const closePopover = () => { - setPopoverOpen(false); - }; - - const goToPage = (newActivePage: number) => { - if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - const toast: Toast = { - id: 'PaginationWarningMsg', - title: headerTitle + i18n.TOAST_TITLE, - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 10000, - text: i18n.TOAST_TEXT, - }; - return dispatchToaster({ - type: 'addToaster', - toast, - }); - } - setActivePage(newActivePage); - loadPage(newActivePage); - updateActivePage(newActivePage); - }; - - const button = ( - <EuiButtonEmpty - size="xs" - color="text" - iconType="arrowDown" - iconSide="right" - onClick={onButtonClick} - > - {`${i18n.ROWS}: ${limit}`} - </EuiButtonEmpty> - ); - - const rowItems = - itemsPerRow && - itemsPerRow.map((item: ItemsPerRow) => ( - <EuiContextMenuItem - key={item.text} - icon={limit === item.numberOfRow ? 'check' : 'empty'} - onClick={() => { - closePopover(); - updateLimitPagination(item.numberOfRow); - updateActivePage(0); // reset results to first page - }} - > - {item.text} - </EuiContextMenuItem> - )); - const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; - - return ( - <InspectButtonContainer show={!loadingInitial}> - <Panel data-test-subj={`${dataTestSubj}-loading-${loading}`} loading={loading}> - <HeaderSection - id={id} - subtitle={ - !loadingInitial && - `${i18n.SHOWING}: ${headerCount >= 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` - } - title={headerTitle} - tooltip={headerTooltip} - > - {!loadingInitial && headerSupplement} - </HeaderSection> - - {loadingInitial ? ( - <EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} /> - ) : ( - <> - <BasicTable - columns={columns} - compressed - items={pageOfItems} - onChange={onChange} - sorting={ - sorting - ? { - sort: { - field: sorting.field, - direction: sorting.direction, - }, - } - : undefined - } - /> - <FooterAction> - <EuiFlexItem> - {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( - <EuiPopover - id="customizablePagination" - data-test-subj="loadingMoreSizeRowPopover" - button={button} - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - > - <EuiContextMenuPanel items={rowItems} data-test-subj="loadingMorePickSizeRow" /> - </EuiPopover> - )} - </EuiFlexItem> - - <PaginationWrapper grow={false}> - <EuiPagination - data-test-subj="numberedPagination" - pageCount={pageCount} - activePage={myActivePage} - onPageClick={goToPage} - /> - </PaginationWrapper> - </FooterAction> - {(isInspect || myLoading) && ( - <Loader data-test-subj="loadingPanelPaginatedTable" overlay size="xl" /> - )} - </> - )} - </Panel> - </InspectButtonContainer> - ); -}; - -export const PaginatedTable = memo(PaginatedTableComponent); - -type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -const BasicTable = styled(EuiBasicTable as BasicTableType)` - tbody { - th, - td { - vertical-align: top; - } - - .euiTableCellContent { - display: block; - } - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -BasicTable.displayName = 'BasicTable'; - -const FooterAction = styled(EuiFlexGroup).attrs(() => ({ - alignItems: 'center', - responsive: false, -}))` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -FooterAction.displayName = 'FooterAction'; - -const PaginationEuiFlexItem = styled(EuiFlexItem)` - @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { - .euiButtonIcon:last-child { - margin-left: 28px; - } - - .euiPagination { - position: relative; - } - - .euiPagination::before { - bottom: 0; - color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; - content: '\\2026'; - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; - padding: 5px ${({ theme }) => theme.eui.euiSizeS}; - position: absolute; - right: ${({ theme }) => theme.eui.euiSizeL}; - } - } -`; - -PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx deleted file mode 100644 index 49afc8d5ef68b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../plugins/siem/common/constants'; -import { TestProviders, mockIndexPattern } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; -import { QueryBar, QueryBarComponentProps } from '.'; -import { createKibanaContextProviderMock } from '../../mock/kibana_react'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -describe('QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockOnChangeQuery = jest.fn(); - const mockOnSubmitQuery = jest.fn(); - const mockOnSavedQuery = jest.fn(); - - beforeEach(() => { - mockOnChangeQuery.mockClear(); - mockOnSubmitQuery.mockClear(); - mockOnSavedQuery.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - <TestProviders> - <QueryBar - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - </TestProviders> - ); - const { - customSubmitButton, - timeHistory, - onClearSavedQuery, - onFiltersUpdated, - onQueryChange, - onQuerySubmit, - onSaved, - onSavedQueryUpdated, - ...searchBarProps - } = wrapper.find(SearchBar).props(); - - expect(searchBarProps).toEqual({ - dataTestSubj: undefined, - dateRangeFrom: 'now-24h', - dateRangeTo: 'now', - filters: [], - indexPatterns: [ - { - fields: [ - { - aggregatable: true, - name: '@timestamp', - searchable: true, - type: 'date', - }, - { - aggregatable: true, - name: '@version', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test1', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test2', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test3', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test4', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test5', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test6', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test7', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test8', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'host.name', - searchable: true, - type: 'string', - }, - ], - title: 'filebeat-*,auditbeat-*,packetbeat-*', - }, - ], - isLoading: false, - isRefreshPaused: true, - query: { - language: 'kuery', - query: 'here: query', - }, - refreshInterval: undefined, - showAutoRefreshOnly: false, - showDatePicker: false, - showFilterBar: true, - showQueryBar: true, - showQueryInput: true, - showSaveQuery: true, - }); - }); - - describe('#onQueryChange', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <KibanaWithStorageProvider services={{ storage: { get: jest.fn() } }}> - <QueryBar {...props} /> - </KibanaWithStorageProvider> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); - queryInput.simulate('change', { target: { value: 'hello: world' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onQuerySubmit', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSubmitQuery: jest.fn() }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onSavedQueryUpdated', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSavedQuery: jest.fn() }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx deleted file mode 100644 index 557d389aefee9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - FilterManager, - Query, - TimeHistory, - TimeRange, - SavedQuery, - SearchBar, - SavedQueryTimeFilter, -} from '../../../../../../../src/plugins/data/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; - -export interface QueryBarComponentProps { - dataTestSubj?: string; - dateRangeFrom?: string; - dateRangeTo?: string; - hideSavedQuery?: boolean; - indexPattern: IIndexPattern; - isLoading?: boolean; - isRefreshPaused?: boolean; - filterQuery: Query; - filterManager: FilterManager; - filters: Filter[]; - onChangedQuery: (query: Query) => void; - onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; - refreshInterval?: number; - savedQuery?: SavedQuery | null; - onSavedQuery: (savedQuery: SavedQuery | null) => void; -} - -export const QueryBar = memo<QueryBarComponentProps>( - ({ - dateRangeFrom, - dateRangeTo, - hideSavedQuery = false, - indexPattern, - isLoading = false, - isRefreshPaused, - filterQuery, - filterManager, - filters, - onChangedQuery, - onSubmitQuery, - refreshInterval, - savedQuery, - onSavedQuery, - dataTestSubj, - }) => { - const [draftQuery, setDraftQuery] = useState(filterQuery); - - useEffect(() => { - setDraftQuery(filterQuery); - }, [filterQuery]); - - const onQuerySubmit = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, filterQuery)) { - onSubmitQuery(payload.query); - } - }, - [filterQuery, onSubmitQuery] - ); - - const onQueryChange = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, draftQuery)) { - setDraftQuery(payload.query); - onChangedQuery(payload.query); - } - }, - [draftQuery, onChangedQuery, setDraftQuery] - ); - - const onSaved = useCallback( - (newSavedQuery: SavedQuery) => { - onSavedQuery(newSavedQuery); - }, - [onSavedQuery] - ); - - const onSavedQueryUpdated = useCallback( - (savedQueryUpdated: SavedQuery) => { - const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; - onSubmitQuery(newQuery, timefilter); - filterManager.setFilters(newFilters || []); - onSavedQuery(savedQueryUpdated); - }, - [filterManager, onSubmitQuery, onSavedQuery] - ); - - const onClearSavedQuery = useCallback(() => { - if (savedQuery != null) { - onSubmitQuery({ - query: '', - language: savedQuery.attributes.query.language, - }); - filterManager.setFilters([]); - onSavedQuery(null); - } - }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); - - const onFiltersUpdated = useCallback( - (newFilters: Filter[]) => { - filterManager.setFilters(newFilters); - }, - [filterManager] - ); - - const CustomButton = <>{null}</>; - const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - - const searchBarProps = savedQuery != null ? { savedQuery } : {}; - - return ( - <SearchBar - customSubmitButton={CustomButton} - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - filters={filters} - indexPatterns={indexPatterns} - isLoading={isLoading} - isRefreshPaused={isRefreshPaused} - query={draftQuery} - onClearSavedQuery={onClearSavedQuery} - onFiltersUpdated={onFiltersUpdated} - onQueryChange={onQueryChange} - onQuerySubmit={onQuerySubmit} - onSaved={onSaved} - onSavedQueryUpdated={onSavedQueryUpdated} - refreshInterval={refreshInterval} - showAutoRefreshOnly={false} - showFilterBar={!hideSavedQuery} - showDatePicker={false} - showQueryBar={true} - showQueryInput={true} - showSaveQuery={true} - timeHistory={new TimeHistory(new Storage(localStorage))} - dataTestSubj={dataTestSubj} - {...searchBarProps} - /> - ); - } -); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx deleted file mode 100644 index 5b851701b973c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ApolloClient from 'apollo-client'; -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { AllTimelinesQuery } from '../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../graphql/types'; -import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; -import { OnOpenTimeline } from '../open_timeline/types'; -import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; -import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; - -import { RecentTimelines } from './recent_timelines'; -import * as i18n from './translations'; -import { FilterMode } from './types'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { navTabs } from '../../pages/home/home_navigations'; -import { getTimelinesUrl } from '../link_to/redirect_to_timelines'; - -interface OwnProps { - apolloClient: ApolloClient<{}>; - filterBy: FilterMode; -} - -export type Props = OwnProps & PropsFromRedux; - -const PAGE_SIZE = 3; - -const StatefulRecentTimelinesComponent = React.memo<Props>( - ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const onOpenTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const noTimelinesMessage = - filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; - const urlSearch = useGetUrlSearch(navTabs.timelines); - const linkAllTimelines = useMemo( - () => <EuiLink href={getTimelinesUrl(urlSearch)}>{i18n.VIEW_ALL_TIMELINES}</EuiLink>, - [urlSearch] - ); - const loadingPlaceholders = useMemo( - () => ( - <LoadingPlaceholders lines={2} placeholders={filterBy === 'favorites' ? 1 : PAGE_SIZE} /> - ), - [filterBy] - ); - - return ( - <AllTimelinesQuery - pageInfo={{ - pageIndex: 1, - pageSize: PAGE_SIZE, - }} - search={''} - sort={{ - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }} - onlyUserFavorite={filterBy === 'favorites'} - > - {({ timelines, loading }) => ( - <> - {loading ? ( - loadingPlaceholders - ) : ( - <RecentTimelines - noTimelinesMessage={noTimelinesMessage} - onOpenTimeline={onOpenTimeline} - timelines={timelines} - /> - )} - <EuiHorizontalRule margin="s" /> - <EuiText size="xs">{linkAllTimelines}</EuiText> - </> - )} - </AllTimelinesQuery> - ); - } -); - -StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx deleted file mode 100644 index a2e75e965bc76..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { apolloClientObservable, mockGlobalState } from '../../mock'; -import { createUseUiSetting$Mock } from '../../mock/kibana_react'; -import { createStore, State } from '../../store'; - -import { SuperDatePicker, makeMapStateToProps } from '.'; -import { cloneDeep } from 'lodash/fp'; - -jest.mock('../../lib/kibana'); -const mockUseUiSetting$ = useUiSetting$ as jest.Mock; -const timepickerRanges = [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - { - from: 'now-15m', - to: 'now', - display: 'Last 15 minutes', - }, - { - from: 'now-30m', - to: 'now', - display: 'Last 30 minutes', - }, - { - from: 'now-1h', - to: 'now', - display: 'Last 1 hour', - }, - { - from: 'now-24h', - to: 'now', - display: 'Last 24 hours', - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - }, -]; - -describe('SIEM Super Date Picker', () => { - describe('#SuperDatePicker', () => { - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - jest.clearAllMocks(); - store = createStore(state, apolloClientObservable); - mockUseUiSetting$.mockImplementation((key, defaultValue) => { - const useUiSetting$Mock = createUseUiSetting$Mock(); - - return key === DEFAULT_TIMEPICKER_QUICK_RANGES - ? [timepickerRanges, jest.fn()] - : useUiSetting$Mock(key, defaultValue); - }); - }); - - describe('Pick Relative Date', () => { - let wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - beforeEach(() => { - wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Make Sure it is relative date', () => { - expect(store.getState().inputs.global.timerange.kind).toBe('relative'); - }); - - test('Make Sure it is last 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 cf350b3993a4b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import dateMath from '@elastic/datemath'; -import { - EuiSuperDatePicker, - OnRefreshChangeProps, - EuiSuperDatePickerRecentRange, - OnRefreshProps, - OnTimeChangeProps, -} from '@elastic/eui'; -import { getOr, take, isEmpty } from 'lodash/fp'; -import React, { useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { inputsModel, State } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { - policySelector, - durationSelector, - kindSelector, - startSelector, - endSelector, - fromStrSelector, - toStrSelector, - isLoadingSelector, - queriesSelector, - kqlQuerySelector, -} from './selectors'; -import { InputsRange } from '../../store/inputs/model'; - -const MAX_RECENTLY_USED_RANGES = 9; - -interface Range { - from: string; - to: string; - display: string; -} - -interface UpdateReduxTime extends OnTimeChangeProps { - id: InputsModelId; - kql?: inputsModel.GlobalKqlQuery | undefined; - timelineId?: string; -} - -interface ReturnUpdateReduxTime { - kqlHaveBeenUpdated: boolean; -} - -export type DispatchUpdateReduxTime = ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime) => ReturnUpdateReduxTime; - -interface OwnProps { - disabled?: boolean; - id: InputsModelId; - timelineId?: string; -} - -export type SuperDatePickerProps = OwnProps & PropsFromRedux; - -export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>( - ({ - duration, - end, - fromStr, - id, - isLoading, - kind, - kqlQuery, - policy, - queries, - setDuration, - start, - startAutoReload, - stopAutoReload, - timelineId, - toStr, - updateReduxTime, - }) => { - const [isQuickSelection, setIsQuickSelection] = useState(true); - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>( - [] - ); - const onRefresh = useCallback( - ({ start: newStart, end: newEnd }: OnRefreshProps): void => { - const { kqlHaveBeenUpdated } = updateReduxTime({ - end: newEnd, - id, - isInvalid: false, - isQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const currentStart = formatDate(newStart); - const currentEnd = isQuickSelection - ? formatDate(newEnd, { roundUp: true }) - : formatDate(newEnd); - if ( - !kqlHaveBeenUpdated && - (!isQuickSelection || (start === currentStart && end === currentEnd)) - ) { - refetchQuery(queries); - } - }, - [end, id, isQuickSelection, kqlQuery, start, timelineId] - ); - - const onRefreshChange = useCallback( - ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { - if (duration !== refreshInterval) { - setDuration({ id, duration: refreshInterval }); - } - - if (isPaused && policy === 'interval') { - stopAutoReload({ id }); - } else if (!isPaused && policy === 'manual') { - startAutoReload({ id }); - } - - if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { - refetchQuery(queries); - } - }, - [id, isQuickSelection, duration, policy, toStr] - ); - - const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { - newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const onTimeChange = useCallback( - ({ - start: newStart, - end: newEnd, - isQuickSelection: newIsQuickSelection, - isInvalid, - }: OnTimeChangeProps) => { - if (!isInvalid) { - updateReduxTime({ - end: newEnd, - id, - isInvalid, - isQuickSelection: newIsQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const newRecentlyUsedRanges = [ - { start: newStart, end: newEnd }, - ...take( - MAX_RECENTLY_USED_RANGES, - recentlyUsedRanges.filter( - recentlyUsedRange => - !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) - ) - ), - ]; - - setRecentlyUsedRanges(newRecentlyUsedRanges); - setIsQuickSelection(newIsQuickSelection); - } - }, - [recentlyUsedRanges, kqlQuery] - ); - - const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); - - const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES); - const commonlyUsedRanges = isEmpty(quickRanges) - ? [] - : quickRanges.map(({ from, to, display }) => ({ - start: from, - end: to, - label: display, - })); - - return ( - <EuiSuperDatePicker - commonlyUsedRanges={commonlyUsedRanges} - end={endDate} - isLoading={isLoading} - isPaused={policy === 'manual'} - onRefresh={onRefresh} - onRefreshChange={onRefreshChange} - onTimeChange={onTimeChange} - recentlyUsedRanges={recentlyUsedRanges} - refreshInterval={duration} - showUpdateButton={true} - start={startDate} - /> - ); - } -); - -export const formatDate = ( - date: string, - options?: { - roundUp?: boolean; - } -) => { - const momentDate = dateMath.parse(date, options); - return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; -}; - -export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime): ReturnUpdateReduxTime => { - const fromDate = formatDate(start); - let toDate = formatDate(end, { roundUp: true }); - if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); - } else { - toDate = formatDate(end); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - id, - from: formatDate(start), - to: formatDate(end), - }) - ); - } - if (timelineId != null) { - dispatch( - timelineActions.updateRange({ - id: timelineId, - start: fromDate, - end: toDate, - }) - ); - } - if (kql) { - return { - kqlHaveBeenUpdated: kql.refetch(dispatch), - }; - } - - return { - kqlHaveBeenUpdated: false, - }; -}; - -export const makeMapStateToProps = () => { - const getDurationSelector = durationSelector(); - const getEndSelector = endSelector(); - const getFromStrSelector = fromStrSelector(); - const getIsLoadingSelector = isLoadingSelector(); - const getKindSelector = kindSelector(); - const getKqlQuerySelector = kqlQuerySelector(); - const getPolicySelector = policySelector(); - const getQueriesSelector = queriesSelector(); - const getStartSelector = startSelector(); - const getToStrSelector = toStrSelector(); - return (state: State, { id }: OwnProps) => { - const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); - return { - duration: getDurationSelector(inputsRange), - end: getEndSelector(inputsRange), - fromStr: getFromStrSelector(inputsRange), - isLoading: getIsLoadingSelector(inputsRange), - kind: getKindSelector(inputsRange), - kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, - policy: getPolicySelector(inputsRange), - queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], - start: getStartSelector(inputsRange), - toStr: getToStrSelector(inputsRange), - }; - }; -}; - -SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - startAutoReload: ({ id }: { id: InputsModelId }) => - dispatch(inputsActions.startAutoReload({ id })), - stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), - setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => - dispatch(inputsActions.setDuration({ id, duration })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx deleted file mode 100644 index c43c69da64ba4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import { mockIndexPattern } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { TestProviders } from '../../../mock/test_providers'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; - -import { TimelineHeader } from '.'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Header', () => { - const indexPattern = mockIndexPattern; - const mount = useMountAppended(); - - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={false} - /> - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the data providers', () => { - const wrapper = mount( - <TestProviders> - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={false} - /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); - }); - - test('it renders the unauthorized call out providers', () => { - const wrapper = mount( - <TestProviders> - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={true} - /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx deleted file mode 100644 index 9fd71f071ec60..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx +++ /dev/null @@ -1,398 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { cloneDeep } from 'lodash/fp'; -import { mockIndexPattern } from '../../mock'; - -import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { buildGlobalQuery, combineQueries } from './helpers'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public'; - -const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); -const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); - -describe('Build KQL Query', () => { - test('Build KQL query with one data provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); - }); - - test('Build KQL query with one data provider as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Buld KQL query with one data provider as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Buld KQL query with one data provider as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider', () => { - const dataProviders = mockDataProviders.slice(0, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); - }); - - test('Build KQL query with one data provider and one and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = mockDataProviders.slice(1, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); - }); - - test('Build KQL query with one data provider and one and as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider and multiple and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); -}); - -describe('Combined Queries', () => { - const config: EsQueryConfig = { - allowLeadingWildcards: true, - queryStringOptions: {}, - ignoreFilterIfFieldNotInIndex: true, - dateFormatTZ: 'America/New_York', - }; - test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - }) - ).toBeNull(); - }); - - test('No Data Provider & No kqlQuery & isEventViewer is true', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', - }); - }); - - test('No Data Provider & No kqlQuery & with Filters', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { query: 'file' }, - type: 'phrase', - }, - query: { match_phrase: { 'event.category': 'file' } }, - }, - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - type: 'exists', - value: 'exists', - }, - exists: { field: 'host.name' }, - } as Filter, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', - }); - }); - - test('Only Data Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with a date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only KQL search/filter query', () => { - const { filterQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx deleted file mode 100644 index f051bbe5b1af6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty, isNumber, get } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; - -import { escapeQueryValue, convertToBuildEsQuery } from '../../lib/keury'; - -import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; -import { BrowserFields } from '../../containers/source'; -import { - IIndexPattern, - Query, - EsQueryConfig, - Filter, -} from '../../../../../../../src/plugins/data/public'; - -const convertDateFieldToQuery = (field: string, value: string | number) => - `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; - -const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { - const baseFields = get('base', browserFields); - if (baseFields != null && baseFields.fields != null) { - return Object.keys(baseFields.fields); - } - return []; -}); - -const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { - const splitFields = field.split('.'); - const baseFields = getBaseFields(browserFields); - if (baseFields.includes(field)) { - return ['base', 'fields', field]; - } - return [splitFields[0], 'fields', field]; -}; - -const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - if (browserField != null && browserField.type === 'date') { - return true; - } - return false; -}; - -const buildQueryMatch = ( - dataProvider: DataProvider | DataProvidersAnd, - browserFields: BrowserFields -) => - `${dataProvider.excluded ? 'NOT ' : ''}${ - dataProvider.queryMatch.operator !== EXISTS_OPERATOR - ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) - ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) - : `${dataProvider.queryMatch.field} : ${ - isNumber(dataProvider.queryMatch.value) - ? dataProvider.queryMatch.value - : escapeQueryValue(dataProvider.queryMatch.value) - }` - : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` - }`.trim(); - -const buildQueryForAndProvider = ( - dataAndProviders: DataProvidersAnd[], - browserFields: BrowserFields -) => - dataAndProviders - .reduce((andQuery, andDataProvider) => { - const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; - return andDataProvider.enabled - ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` - : andQuery; - }, '') - .trim(); - -export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => - dataProviders - .reduce((query, dataProvider: DataProvider, i) => { - const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; - const openParen = i > 0 ? '(' : ''; - const closeParen = i > 0 ? ')' : ''; - return dataProvider.enabled - ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} - ${ - dataProvider.and.length > 0 - ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` - : '' - }${closeParen}`.trim() - : query; - }, '') - .trim(); - -export const combineQueries = ({ - config, - dataProviders, - indexPattern, - browserFields, - filters = [], - kqlQuery, - kqlMode, - start, - end, - isEventViewer, -}: { - config: EsQueryConfig; - dataProviders: DataProvider[]; - indexPattern: IIndexPattern; - browserFields: BrowserFields; - filters: Filter[]; - kqlQuery: Query; - kqlMode: string; - start: number; - end: number; - isEventViewer?: boolean; -}): { filterQuery: string } | null => { - const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { - return null; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { - kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { - kuery.query = `(${buildGlobalQuery( - dataProviders, - browserFields - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } - const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; - const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; - kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( - kqlQuery.query as string - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; -}; - -/** - * The CSS class name of a "stateful event", which appears in both - * the `Timeline` and the `Events Viewer` widget - */ -export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx deleted file mode 100644 index bf953cfd006aa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiModal, - EuiOverlayMask, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; - -import { Note } from '../../../lib/note'; -import { Notes } from '../../notes'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; -import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; -import * as i18n from './translations'; -import { SiemPageName } from '../../../pages/home/types'; -import { timelineSelectors } from '../../../store/timeline'; -import { State } from '../../../store'; - -export const historyToolTip = 'The chronological history of actions related to this timeline'; -export const streamLiveToolTip = 'Update the Timeline as new data arrives'; -export const newTimelineToolTip = 'Create a new timeline'; - -const NotesCountBadge = styled(EuiBadge)` - margin-left: 5px; -` 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 a49f6cc930abd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../../plugins/siem/common/constants'; -import { mockBrowserFields } from '../../../containers/source/mock'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { mockIndexPattern, TestProviders } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { QueryBar } from '../../query_bar'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { buildGlobalQuery } from '../helpers'; - -import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Timeline QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockApplyKqlFilterQuery = jest.fn(); - const mockSetFilters = jest.fn(); - const mockSetKqlFilterQueryDraft = jest.fn(); - const mockSetSavedQueryId = jest.fn(); - const mockUpdateReduxTime = jest.fn(); - - beforeEach(() => { - mockApplyKqlFilterQuery.mockClear(); - mockSetFilters.mockClear(); - mockSetKqlFilterQueryDraft.mockClear(); - mockSetSavedQueryId.mockClear(); - mockUpdateReduxTime.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - <TestProviders> - <QueryBarTimeline - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - </TestProviders> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - - expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); - expect(queryBarProps.dateRangeTo).toEqual('now'); - expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); - expect(queryBarProps.savedQuery).toEqual(null); - }); - - describe('#onChangeQuery', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSubmitQuery', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ timelineId: 'new-timeline' }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSavedQuery', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - - test('is only reference that changed when savedQueryId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ - savedQueryId: 'new', - }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - }); - - describe('#getDataProviderFilter', () => { - test('returns valid data provider filter with a simple bool data provider', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - name: 'Provider 1', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', - }, - }); - }); - - test('returns valid data provider filter with an exists operator', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery( - [ - { - id: `id-exists`, - name, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: '', - operator: ':*', - }, - and: [], - }, - ], - mockBrowserFields - ), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx deleted file mode 100644 index f53f7bb56e2f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useState, useEffect } from 'react'; -import { Subscription } from 'rxjs'; -import deepEqual from 'fast-deep-equal'; - -import { - IIndexPattern, - Query, - Filter, - esFilters, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; -import { KqlMode } from '../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../utils/saved_query_services'; -import { DispatchUpdateReduxTime } from '../../super_date_picker'; -import { QueryBar } from '../../query_bar'; -import { DataProvider } from '../data_providers/data_provider'; -import { buildGlobalQuery } from '../helpers'; - -export interface QueryBarTimelineComponentProps { - applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; - browserFields: BrowserFields; - dataProviders: DataProvider[]; - filters: Filter[]; - filterManager: FilterManager; - filterQuery: KueryFilterQuery; - filterQueryDraft: KueryFilterQuery; - from: number; - fromStr: string; - kqlMode: KqlMode; - indexPattern: IIndexPattern; - isRefreshPaused: boolean; - refreshInterval: number; - savedQueryId: string | null; - setFilters: (filters: Filter[]) => void; - setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; - setSavedQueryId: (savedQueryId: string | null) => void; - timelineId: string; - to: number; - toStr: string; - updateReduxTime: DispatchUpdateReduxTime; -} - -const timelineFilterDropArea = 'timeline-filter-drop-area'; - -export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - kqlMode, - indexPattern, - isRefreshPaused, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - refreshInterval, - timelineId, - to, - toStr, - updateReduxTime, - }) => { - const [dateRangeFrom, setDateRangeFrom] = useState<string>( - fromStr != null ? fromStr : new Date(from).toISOString() - ); - const [dateRangeTo, setDateRangTo] = useState<string>( - toStr != null ? toStr : new Date(to).toISOString() - ); - - const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); - const [filterQueryConverted, setFilterQueryConverted] = useState<Query>({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - const [queryBarFilters, setQueryBarFilters] = useState<Filter[]>([]); - const [dataProvidersDsl, setDataProvidersDsl] = useState<string>( - convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) - ); - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters(filters); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - setFilters(filterWithoutDropArea); - setQueryBarFilters(filterWithoutDropArea); - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, []); - - useEffect(() => { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - if (!deepEqual(filters, filterWithoutDropArea)) { - filterManager.setFilters(filters); - } - }, [filters]); - - useEffect(() => { - setFilterQueryConverted({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - }, [filterQuery]); - - useEffect(() => { - setDataProvidersDsl( - convertKueryToElasticSearchQuery( - buildGlobalQuery(dataProviders, browserFields), - indexPattern - ) - ); - }, [dataProviders, browserFields, indexPattern]); - - useEffect(() => { - if (fromStr != null && toStr != null) { - setDateRangeFrom(fromStr); - setDateRangTo(toStr); - } else if (from != null && to != null) { - setDateRangeFrom(new Date(from).toISOString()); - setDateRangTo(new Date(to).toISOString()); - } - }, [from, fromStr, to, toStr]); - - useEffect(() => { - let isSubscribed = true; - async function setSavedQueryByServices() { - if (savedQueryId != null && savedQueryServices != null) { - try { - // The getSavedQuery function will throw a promise rejection in - // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts - // if the savedObjectsClient is undefined. This is happening in a test - // so I wrapped this in a try catch to keep the unhandled promise rejection - // warning from appearing in tests. - const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery({ - ...mySavedQuery, - attributes: { - ...mySavedQuery.attributes, - filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), - }, - }); - } - } catch (exc) { - setSavedQuery(null); - } - } else if (isSubscribed) { - setSavedQuery(null); - } - } - setSavedQueryByServices(); - return () => { - isSubscribed = false; - }; - }, [savedQueryId]); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - if ( - filterQueryDraft == null || - (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || - filterQueryDraft.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - } - }, - [filterQueryDraft] - ); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - if ( - filterQuery == null || - (filterQuery != null && filterQuery.expression !== newQuery.query) || - filterQuery.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); - } - if (timefilter != null) { - const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); - - updateReduxTime({ - id: 'timeline', - end: timefilter.to, - start: timefilter.from, - isInvalid: false, - isQuickSelection, - timelineId, - }); - } - }, - [filterQuery, timelineId] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - if (newSavedQuery.id !== savedQueryId) { - setSavedQueryId(newSavedQuery.id); - } - if (savedQueryServices != null && dataProvidersDsl !== '') { - const dataProviderFilterExists = - newSavedQuery.attributes.filters != null - ? newSavedQuery.attributes.filters.findIndex( - f => f.meta.controlledBy === timelineFilterDropArea - ) - : -1; - savedQueryServices.saveQuery( - { - ...newSavedQuery.attributes, - filters: - newSavedQuery.attributes.filters != null - ? dataProviderFilterExists > -1 - ? [ - ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), - getDataProviderFilter(dataProvidersDsl), - ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), - ] - : [ - ...newSavedQuery.attributes.filters, - getDataProviderFilter(dataProvidersDsl), - ] - : [], - }, - { - overwrite: true, - } - ); - } - } else { - setSavedQueryId(null); - } - }, - [dataProvidersDsl, savedQueryId, savedQueryServices] - ); - - return ( - <QueryBar - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - hideSavedQuery={kqlMode === 'search'} - indexPattern={indexPattern} - isRefreshPaused={isRefreshPaused} - filterQuery={filterQueryConverted} - filterManager={filterManager} - filters={queryBarFilters} - onChangedQuery={onChangedQuery} - onSubmitQuery={onSubmitQuery} - refreshInterval={refreshInterval} - savedQuery={savedQuery} - onSavedQuery={onSavedQuery} - dataTestSubj={'timelineQueryInput'} - /> - ); - } -); - -export const getDataProviderFilter = (dataProviderDsl: string): Filter => { - const dslObject = JSON.parse(dataProviderDsl); - const key = Object.keys(dslObject); - return { - ...dslObject, - meta: { - alias: timelineFilterDropArea, - controlledBy: timelineFilterDropArea, - negate: false, - disabled: false, - type: 'custom', - key: isEmpty(key) ? 'bool' : key[0], - value: dataProviderDsl, - }, - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx deleted file mode 100644 index a0a0ac4c2b85c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { - KueryFilterQuery, - SerializedFilterQuery, - State, - timelineSelectors, - inputsModel, - inputsSelectors, -} from '../../../store'; -import { timelineActions } from '../../../store/actions'; -import { KqlMode, TimelineModel, EventType } from '../../../store/timeline/model'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { dispatchUpdateReduxTime } from '../../super_date_picker'; -import { SearchOrFilter } from './search_or_filter'; - -interface OwnProps { - browserFields: BrowserFields; - filterManager: FilterManager; - indexPattern: IIndexPattern; - timelineId: string; -} - -type Props = OwnProps & PropsFromRedux; - -const StatefulSearchOrFilterComponent = React.memo<Props>( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - eventType, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - indexPattern, - isRefreshPaused, - kqlMode, - refreshInterval, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - timelineId, - to, - toStr, - updateEventType, - updateKqlMode, - updateReduxTime, - }) => { - const applyFilterQueryFromKueryExpression = useCallback( - (expression: string, kind) => - applyKqlFilterQuery({ - id: timelineId, - filterQuery: { - kuery: { - kind, - expression, - }, - serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), - }, - }), - [indexPattern, timelineId] - ); - - const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string, kind) => - setKqlFilterQueryDraft({ - id: timelineId, - filterQueryDraft: { - kind, - expression, - }, - }), - [timelineId] - ); - - const setFiltersInTimeline = useCallback( - (newFilters: Filter[]) => - setFilters({ - id: timelineId, - filters: newFilters, - }), - [timelineId] - ); - - const setSavedQueryInTimeline = useCallback( - (newSavedQueryId: string | null) => - setSavedQueryId({ - id: timelineId, - savedQueryId: newSavedQueryId, - }), - [timelineId] - ); - - const handleUpdateEventType = useCallback( - (newEventType: EventType) => - updateEventType({ - id: timelineId, - eventType: newEventType, - }), - [timelineId] - ); - - return ( - <SearchOrFilter - applyKqlFilterQuery={applyFilterQueryFromKueryExpression} - browserFields={browserFields} - dataProviders={dataProviders} - eventType={eventType} - filters={filters} - filterManager={filterManager} - filterQuery={filterQuery} - filterQueryDraft={filterQueryDraft} - from={from} - fromStr={fromStr} - indexPattern={indexPattern} - isRefreshPaused={isRefreshPaused} - kqlMode={kqlMode!} - refreshInterval={refreshInterval} - savedQueryId={savedQueryId} - setFilters={setFiltersInTimeline} - setKqlFilterQueryDraft={setFilterQueryDraftFromKueryExpression!} - setSavedQueryId={setSavedQueryInTimeline} - timelineId={timelineId} - to={to} - toStr={toStr} - updateEventType={handleUpdateEventType} - updateKqlMode={updateKqlMode!} - updateReduxTime={updateReduxTime} - /> - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.eventType === nextProps.eventType && - prevProps.filterManager === nextProps.filterManager && - prevProps.from === nextProps.from && - prevProps.fromStr === nextProps.fromStr && - prevProps.to === nextProps.to && - prevProps.toStr === nextProps.toStr && - prevProps.isRefreshPaused === nextProps.isRefreshPaused && - prevProps.refreshInterval === nextProps.refreshInterval && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.filterQuery, nextProps.filterQuery) && - deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && - deepEqual(prevProps.kqlMode, nextProps.kqlMode) && - deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - deepEqual(prevProps.timelineId, nextProps.timelineId) - ); - } -); -StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const input: inputsModel.InputsRange = getInputsTimeline(state); - const policy: inputsModel.Policy = getInputsPolicy(state); - return { - dataProviders: timeline.dataProviders, - eventType: timeline.eventType ?? 'raw', - filterQuery: getKqlFilterQuery(state, timelineId)!, - filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, - filters: timeline.filters!, - from: input.timerange.from, - fromStr: input.timerange.fromStr!, - isRefreshPaused: policy.kind === 'manual', - kqlMode: getOr('filter', 'kqlMode', timeline), - refreshInterval: policy.duration, - savedQueryId: getOr(null, 'savedQueryId', timeline), - to: input.timerange.to, - toStr: input.timerange.toStr!, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => - dispatch( - timelineActions.applyKqlFilterQuery({ - id, - filterQuery, - }) - ), - updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => - dispatch(timelineActions.updateEventType({ id, eventType })), - updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => - dispatch(timelineActions.updateKqlMode({ id, kqlMode })), - setKqlFilterQueryDraft: ({ - id, - filterQueryDraft, - }: { - id: string; - filterQueryDraft: KueryFilterQuery; - }) => - dispatch( - timelineActions.setKqlFilterQueryDraft({ - id, - filterQueryDraft, - }) - ), - setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => - dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), - setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => - dispatch(timelineActions.setFilters({ id, filters })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx deleted file mode 100644 index 639d30bbe7bb9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiSelectable, - EuiHighlight, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiTextColor, - EuiSelectableOption, - EuiPortal, - EuiFilterGroup, - EuiFilterButton, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useMemo, useState } from 'react'; -import { ListProps } from 'react-virtualized'; -import styled from 'styled-components'; - -import { AllTimelinesQuery } from '../../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../../graphql/types'; -import { isUntitled } from '../../open_timeline/helpers'; -import * as i18nTimeline from '../../open_timeline/translations'; -import { OpenTimelineResult } from '../../open_timeline/types'; -import { getEmptyTagValue } from '../../empty_value'; - -import * as i18n from '../translations'; - -const MyEuiFlexItem = styled(EuiFlexItem)` - display: inline-block; - max-width: 296px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - padding 0px 4px; -`; - -const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` - .euiSelectable { - .euiFormControlLayout__childrenWrapper { - display: flex; - } - ${({ isLoading }) => `${ - isLoading - ? ` - .euiFormControlLayoutIcons { - display: none; - } - .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { - display: block; - left: 12px; - top: 12px; - }` - : '' - } - `} - } -`; - -const ORIGINAL_PAGE_SIZE = 50; -const POPOVER_HEIGHT = 260; -const TIMELINE_ITEM_HEIGHT = 50; - -export interface GetSelectableOptions { - timelines: OpenTimelineResult[]; - onlyFavorites: boolean; - searchTimelineValue: string; -} - -interface SelectableTimelineProps { - hideUntitled?: boolean; - getSelectableOptions: ({ - timelines, - onlyFavorites, - searchTimelineValue, - }: GetSelectableOptions) => EuiSelectableOption[]; - onClosePopover: () => void; - onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; -} - -const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({ - hideUntitled = false, - getSelectableOptions, - onClosePopover, - onTimelineChange, -}) => { - const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); - const [heightTrigger, setHeightTrigger] = useState(0); - const [searchTimelineValue, setSearchTimelineValue] = useState(''); - const [onlyFavorites, setOnlyFavorites] = useState(false); - const [searchRef, setSearchRef] = useState<HTMLElement | null>(null); - - const onSearchTimeline = useCallback(val => { - setSearchTimelineValue(val); - }, []); - - const handleOnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - const handleOnScroll = useCallback( - ( - totalTimelines: number, - totalCount: number, - { - clientHeight, - scrollHeight, - scrollTop, - }: { - clientHeight: number; - scrollHeight: number; - scrollTop: number; - } - ) => { - if (totalTimelines < totalCount) { - const clientHeightTrigger = clientHeight * 1.2; - if ( - scrollTop > 10 && - scrollHeight - scrollTop < clientHeightTrigger && - scrollHeight > heightTrigger - ) { - setHeightTrigger(scrollHeight); - setPageSize(pageSize + ORIGINAL_PAGE_SIZE); - } - } - }, - [heightTrigger, pageSize] - ); - - const renderTimelineOption = useCallback((option, searchValue) => { - return ( - <EuiFlexGroup - gutterSize="s" - justifyContent="spaceBetween" - alignItems="center" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiFlexGroup gutterSize="none" direction="column"> - <MyEuiFlexItem 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 61772f1dc7a7a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../containers/source/mock'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { createStore, State } from '../../store'; -import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; - -import { Props } from './top_n'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; - -jest.mock('../../lib/kibana'); - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -const field = 'process.name'; -const value = 'nice'; - -const state: State = { - ...mockGlobalState, - inputs: { - ...mockGlobalState.inputs, - global: { - ...mockGlobalState.inputs.global, - query: { - query: 'host.name : end*', - language: 'kuery', - }, - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { - query: 'Linux', - }, - }, - query: { - match: { - 'host.os.name': { - query: 'Linux', - type: 'phrase', - }, - }, - }, - }, - ], - }, - timeline: { - ...mockGlobalState.inputs.timeline, - timerange: { - kind: 'relative', - fromStr: 'now-24h', - toStr: 'now', - from: 1586835969047, - to: 1586922369047, - }, - }, - }, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [ACTIVE_TIMELINE_REDUX_ID]: { - ...mockGlobalState.timeline.timelineById.test, - id: ACTIVE_TIMELINE_REDUX_ID, - dataProviders: [ - { - id: - 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', - name: 'tcp', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'network.transport', - value: 'tcp', - operator: ':', - }, - and: [], - }, - ], - eventType: 'all', - filters: [ - { - meta: { - alias: null, - disabled: false, - key: 'source.port', - negate: false, - params: { - query: '30045', - }, - type: 'phrase', - }, - query: { - match: { - 'source.port': { - query: '30045', - type: 'phrase', - }, - }, - }, - }, - ], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'host.name : *', - }, - serializedQuery: - '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - filterQueryDraft: { - kind: 'kuery', - expression: 'host.name : *', - }, - }, - }, - }, - }, -}; -const store = createStore(state, apolloClientObservable); - -describe('StatefulTopN', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - - describe('rendering in a global NON-timeline context', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - <TestProviders store={store}> - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TestProviders> - ); - }); - - test('it has undefined combinedQueries when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toBeUndefined(); - }); - - test(`defaults to the 'Raw events' view when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('raw'); - }); - - test(`provides a 'deleteQuery' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeDefined(); - }); - - test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { query: 'Linux' }, - }, - query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, - }, - ]); - }); - - test(`provides 'from' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(0); - }); - - test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); - }); - - test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); - }); - - test(`provides 'to' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1); - }); - }); - - describe('rendering in a timeline context', () => { - let filterManager: FilterManager; - let wrapper: ReactWrapper; - - beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - - wrapper = mount( - <TestProviders store={store}> - <TimelineContext.Provider value={{ filterManager, isLoading: false }}> - <TimelineTypeContext.Provider value={{ id: ACTIVE_TIMELINE_REDUX_ID }}> - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TimelineTypeContext.Provider> - </TimelineContext.Provider> - </TestProviders> - ); - }); - - test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' - ); - }); - - test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('all'); - }); - - test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeUndefined(); - }); - - test(`provides empty filters when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([]); - }); - - test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(1586835969047); - }); - - test('provides an empty query when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: '', language: 'kuery' }); - }); - - test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); - }); - - test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1586922369047); - }); - }); - - test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { - const filterManager = new FilterManager(mockUiSettingsForFilterManager); - const wrapper = mount( - <TestProviders store={store}> - <TimelineContext.Provider value={{ filterManager, isLoading: false }}> - <TimelineTypeContext.Provider - value={{ documentType: 'signals', id: ACTIVE_TIMELINE_REDUX_ID }} - > - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TimelineTypeContext.Provider> - </TimelineContext.Provider> - </TestProviders> - ); - - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('signal'); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx deleted file mode 100644 index 983b234a04eaa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { GlobalTime } from '../../containers/global_time'; -import { BrowserFields, WithSource } from '../../containers/source'; -import { useKibana } from '../../lib/kibana'; -import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { TimelineModel } from '../../store/timeline/model'; -import { combineQueries } from '../timeline/helpers'; -import { useTimelineTypeContext } from '../timeline/timeline_context'; - -import { getOptions } from './helpers'; -import { TopN } from './top_n'; - -/** The currently active timeline always has this Redux ID */ -export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; - -const EMPTY_FILTERS: Filter[] = []; -const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); - - // The mapped Redux state provided to this component includes the global - // filters that appear at the top of most views in the app, and all the - // filters in the active timeline: - const mapStateToProps = (state: State) => { - const activeTimeline: TimelineModel = - getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; - const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; - const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); - - return { - activeTimelineEventType: activeTimeline.eventType, - activeTimelineFilters, - activeTimelineFrom: activeTimelineInput.timerange.from, - activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), - activeTimelineTo: activeTimelineInput.timerange.to, - dataProviders: activeTimeline.dataProviders, - globalQuery: getGlobalQuerySelector(state), - globalFilters: getGlobalFiltersQuerySelector(state), - kqlMode: activeTimeline.kqlMode, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -interface OwnProps { - browserFields: BrowserFields; - field: string; - toggleTopN: () => void; - onFilterAdded?: () => void; - value?: string[] | string | null; -} -type PropsFromRedux = ConnectedProps<typeof connector>; -type Props = OwnProps & PropsFromRedux; - -const StatefulTopNComponent: React.FC<Props> = ({ - activeTimelineEventType, - activeTimelineFilters, - activeTimelineFrom, - activeTimelineKqlQueryExpression, - activeTimelineTo, - browserFields, - dataProviders, - field, - globalFilters = EMPTY_FILTERS, - globalQuery = EMPTY_QUERY, - kqlMode, - onFilterAdded, - setAbsoluteRangeDatePicker, - toggleTopN, - value, -}) => { - const kibana = useKibana(); - - // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'signals') may only be populated in some views, - // e.g. the `Signals` view on the `Detections` page. - // * `id` (`timelineId`) may only be populated when we are rendered in the - // context of the active timeline. - // * `indexToAdd`, which enables the signals index to be appended to - // the `indexPattern` returned by `WithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the signals index - // to the index pattern. - const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); - - const options = getOptions( - timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined - ); - - return ( - <GlobalTime> - {({ from, deleteQuery, setQuery, to }) => ( - <WithSource sourceId="default" indexToAdd={indexToAdd}> - {({ indexPattern }) => ( - <TopN - combinedQueries={ - timelineId === ACTIVE_TIMELINE_REDUX_ID - ? combineQueries({ - browserFields, - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - end: activeTimelineTo, - filters: activeTimelineFilters, - indexPattern, - kqlMode, - kqlQuery: { - language: 'kuery', - query: activeTimelineKqlQueryExpression ?? '', - }, - start: activeTimelineFrom, - })?.filterQuery - : undefined - } - data-test-subj="top-n" - defaultView={ - documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value - } - deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} - field={field} - filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} - from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} - indexPattern={indexPattern} - indexToAdd={indexToAdd} - options={options} - query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} - setAbsoluteRangeDatePickerTarget={ - timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' - } - setQuery={setQuery} - to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} - toggleTopN={toggleTopN} - onFilterAdded={onFilterAdded} - value={value} - /> - )} - </WithSource> - )} - </GlobalTime> - ); -}; - -StatefulTopNComponent.displayName = 'StatefulTopNComponent'; - -export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts deleted file mode 100644 index b30244e57d0f1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { parse, stringify } from 'query-string'; -import { decode, encode } from 'rison-node'; -import * as H from 'history'; - -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { url } from '../../../../../../../src/plugins/kibana_utils/public'; - -import { SiemPageName } from '../../pages/home/types'; -import { inputsSelectors, State, timelineSelectors } from '../../store'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { formatDate } from '../super_date_picker'; -import { NavTab } from '../navigation/types'; -import { CONSTANTS, UrlStateType } from './constants'; -import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; - -export const decodeRisonUrlState = <T>(value: string | undefined): T | null => { - try { - return value ? ((decode(value) as unknown) as T) : null; - } catch (error) { - if (error instanceof Error && error.message.startsWith('rison decoder error')) { - return null; - } - throw error; - } -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const encodeRisonUrlState = (state: any) => encode(state); - -export const getQueryStringFromLocation = (search: string) => search.substring(1); - -export const getParamFromQueryString = (queryString: string, key: string) => { - const parsedQueryString = parse(queryString, { sort: false }); - const queryParam = parsedQueryString[key]; - - return Array.isArray(queryParam) ? queryParam[0] : queryParam; -}; - -export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => ( - queryString: string -): string => { - const previousQueryValues = parse(queryString, { sort: false }); - if (urlState == null || (typeof urlState === 'string' && urlState === '')) { - delete previousQueryValues[stateKey]; - - return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); - } - - // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ - // Remove this if these utilities are promoted to kibana core - const encodedUrlState = - typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - - return stringify( - url.encodeQuery({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }), - { sort: false, encode: false } - ); -}; - -export const replaceQueryStringInLocation = ( - location: H.Location, - queryString: string -): H.Location => { - if (queryString === getQueryStringFromLocation(location.search)) { - return location; - } else { - return { - ...location, - search: `?${queryString}`, - }; - } -}; - -export const getUrlType = (pageName: string): UrlStateType => { - if (pageName === SiemPageName.overview) { - return 'overview'; - } else if (pageName === SiemPageName.hosts) { - return 'host'; - } else if (pageName === SiemPageName.network) { - return 'network'; - } else if (pageName === SiemPageName.detections) { - return 'detections'; - } else if (pageName === SiemPageName.timelines) { - return 'timeline'; - } else if (pageName === SiemPageName.case) { - return 'case'; - } - return 'overview'; -}; - -export const getTitle = ( - pageName: string, - detailName: string | undefined, - navTabs: Record<string, NavTab> -): string => { - if (detailName != null) return detailName; - return navTabs[pageName] != null ? navTabs[pageName].name : ''; -}; - -export const makeMapStateToProps = () => { - const getInputsSelector = inputsSelectors.inputsSelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); - const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State) => { - const inputState = getInputsSelector(state); - const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; - const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - - const timeline = Object.entries(getTimelines(state)).reduce( - (obj, [timelineId, timelineObj]) => ({ - id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', - isOpen: timelineObj.show, - }), - { id: '', isOpen: false } - ); - - let searchAttr: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - } = { - [CONSTANTS.appQuery]: getGlobalQuerySelector(state), - [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), - }; - const savedQuery = getGlobalSavedQuerySelector(state); - if (savedQuery != null && savedQuery.id !== '') { - searchAttr = { - [CONSTANTS.savedQuery]: savedQuery.id, - }; - } - - return { - urlState: { - ...searchAttr, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: globalTimerange, - linkTo: globalLinkTo, - }, - timeline: { - [CONSTANTS.timerange]: timelineTimerange, - linkTo: timelineLinkTo, - }, - }, - [CONSTANTS.timeline]: timeline, - }, - }; - }; - - return mapStateToProps; -}; - -export const updateTimerangeUrl = ( - timeRange: UrlInputsModel, - isInitializing: boolean -): UrlInputsModel => { - if (timeRange.global.timerange.kind === 'relative') { - timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); - timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); - } - if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { - timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); - timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { - roundUp: true, - }); - } - return timeRange; -}; - -export const updateUrlStateString = ({ - isInitializing, - history, - newUrlStateString, - pathName, - search, - updateTimerange, - urlKey, -}: UpdateUrlStateString): string => { - if (urlKey === CONSTANTS.appQuery) { - const queryState = decodeRisonUrlState<Query>(newUrlStateString); - if (queryState != null && queryState.query === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timerange && updateTimerange) { - const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString); - if (queryState != null && queryState.global != null) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.filters) { - const queryState = decodeRisonUrlState<Filter[]>(newUrlStateString); - if (isEmpty(queryState)) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timeline) { - const queryState = decodeRisonUrlState<TimelineUrl>(newUrlStateString); - if (queryState != null && queryState.id === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } - return search; -}; - -export const replaceStateInLocation = <T>({ - history, - urlStateToReplace, - urlStateKey, - pathName, - search, -}: ReplaceStateInLocation<T>) => { - const newLocation = replaceQueryStringInLocation( - { - hash: '', - pathname: pathName, - search, - state: '', - }, - replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) - ); - if (history) { - history.replace(newLocation); - } - return newLocation.search; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx deleted file mode 100644 index 83c38f2a76175..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; - -import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; -import { AnomaliesQueryTabBodyProps } from './types'; -import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -const ID = 'anomaliesOverTimeQuery'; - -export const AnomaliesQueryTabBody = ({ - deleteQuery, - endDate, - setQuery, - skip, - startDate, - type, - narrowDateRange, - filterQuery, - anomaliesFilterQuery, - AnomaliesTableComponent, - flowTarget, - ip, -}: AnomaliesQueryTabBodyProps) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const [, siemJobs] = useSiemJobs(true); - const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE); - - const mergedFilterQuery = getAnomaliesFilterQuery( - filterQuery, - anomaliesFilterQuery, - siemJobs, - anomalyScore, - flowTarget, - ip - ); - - return ( - <> - <MatrixHistogramContainer - endDate={endDate} - filterQuery={mergedFilterQuery} - id={ID} - setQuery={setQuery} - sourceId="default" - startDate={startDate} - type={type} - {...histogramConfigs} - /> - <AnomaliesTableComponent - startDate={startDate} - endDate={endDate} - skip={skip} - type={type as never} - narrowDateRange={narrowDateRange} - flowTarget={flowTarget} - ip={ip} - /> - </> - ); -}; - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts deleted file mode 100644 index d17eadc68d04b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { UpdateDateRange } from '../../../components/charts/common'; -import { SetQuery } from '../../../pages/hosts/navigation/types'; -import { FlowTarget } from '../../../graphql/types'; -import { HostsType } from '../../../store/hosts/model'; -import { NetworkType } from '../../../store/network/model'; -import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; - -interface QueryTabBodyProps { - type: HostsType | NetworkType; - filterQuery?: string | ESTermQuery; -} - -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { - anomaliesFilterQuery?: object; - AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; - deleteQuery?: ({ id }: { id: string }) => void; - endDate: number; - flowTarget?: FlowTarget; - narrowDateRange: NarrowDateRange; - setQuery: SetQuery; - startDate: number; - skip: boolean; - updateDateRange?: UpdateDateRange; - hideHistogramIfEmpty?: boolean; - ip?: string; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts deleted file mode 100644 index f698e302d3423..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import deepmerge from 'deepmerge'; - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; -import { FlowTarget } from '../../../graphql/types'; - -export const getAnomaliesFilterQuery = ( - filterQuery: string | ESTermQuery | undefined, - anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], - anomalyScore: number, - flowTarget?: FlowTarget, - ip?: string -): string => { - const siemJobIds = siemJobs - .filter(job => job.isInstalled) - .map(job => job.id) - .map(jobId => ({ - match_phrase: { - job_id: jobId, - }, - })); - - const filterQueryString = createFilter(filterQuery); - const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; - const mergedFilterQuery = deepmerge.all([ - filterQueryObject, - anomaliesFilterQuery, - { - bool: { - filter: [ - { - bool: { - should: siemJobIds, - minimum_should_match: 1, - }, - }, - { - match_phrase: { - result_type: 'record', - }, - }, - flowTarget && - ip && { - match_phrase: { - [`${flowTarget}.ip`]: ip, - }, - }, - { - range: { - record_score: { - gte: anomalyScore, - }, - }, - }, - ], - }, - }, - ]); - - return JSON.stringify(mergedFilterQuery); -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx deleted file mode 100644 index 13bb40dad04bd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - AuthenticationsEdges, - GetAuthenticationsQuery, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { authenticationsQuery } from './index.gql_query'; - -const ID = 'authenticationQuery'; - -export interface AuthenticationArgs { - authentications: AuthenticationsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: AuthenticationArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -export interface AuthenticationsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; -} - -type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; - -class AuthenticationsComponentQuery extends QueryTemplatePaginated< - AuthenticationsProps, - GetAuthenticationsQuery.Query, - GetAuthenticationsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetAuthenticationsQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }; - return ( - <Query<GetAuthenticationsQuery.Query, GetAuthenticationsQuery.Variables> - query={authenticationsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const authentications = getOr([], 'source.Authentications.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Authentications: { - ...fetchMoreResult.source.Authentications, - edges: [...fetchMoreResult.source.Authentications.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - authentications, - id, - inspect: getOr(null, 'source.Authentications.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Authentications.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Authentications.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getAuthenticationsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const AuthenticationsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(AuthenticationsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts deleted file mode 100644 index 6d2cfb7147537..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ActionLicense, - AllCases, - BulkUpdateStatus, - Case, - CasesStatus, - CaseUserActions, - FetchCasesProps, - SortFieldCase, -} from '../types'; -import { - actionLicenses, - allCases, - basicCase, - basicCaseCommentPatch, - basicCasePost, - casesStatus, - caseUserActions, - pushedCase, - respReporters, - serviceConnector, - tags, -} from '../mock'; -import { - CaseExternalServiceRequest, - CasePatchRequest, - CasePostRequest, - CommentRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, - User, -} from '../../../../../../../plugins/case/common/api'; - -export const getCase = async ( - caseId: string, - includeComments: boolean = true, - signal: AbortSignal -): Promise<Case> => { - return Promise.resolve(basicCase); -}; - -export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => - Promise.resolve(casesStatus); - -export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); - -export const getReporters = async (signal: AbortSignal): Promise<User[]> => - Promise.resolve(respReporters); - -export const getCaseUserActions = async ( - caseId: string, - signal: AbortSignal -): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); - -export const getCases = async ({ - filterOptions = { - search: '', - reporters: [], - status: 'open', - tags: [], - }, - queryParams = { - page: 1, - perPage: 5, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - signal, -}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); - -export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => - Promise.resolve(basicCasePost); - -export const patchCase = async ( - caseId: string, - updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, - version: string, - signal: AbortSignal -): Promise<Case[]> => Promise.resolve([basicCase]); - -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], - signal: AbortSignal -): Promise<Case[]> => Promise.resolve(allCases.cases); - -export const postComment = async ( - newComment: CommentRequest, - caseId: string, - signal: AbortSignal -): Promise<Case> => Promise.resolve(basicCase); - -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal -): Promise<Case> => Promise.resolve(basicCaseCommentPatch); - -export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => - Promise.resolve(true); - -export const pushCase = async ( - caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise<Case> => Promise.resolve(pushedCase); - -export const pushToService = async ( - connectorId: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise<ServiceConnectorCaseResponse> => Promise.resolve(serviceConnector); - -export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => - Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts deleted file mode 100644 index b745361632419..0000000000000 --- 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 03f7d241e5dff..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - CasesConfigurePatch, - CasesConfigureRequest, - Connector, -} from '../../../../../../../../plugins/case/common/api'; - -import { ApiProps } from '../../types'; -import { CaseConfigure } from '../types'; -import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; - -export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => - Promise.resolve(connectorsMock); - -export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => - Promise.resolve(caseConfigurationCamelCaseResponseMock); - -export const postCaseConfigure = async ( - caseConfiguration: CasesConfigureRequest, - signal: AbortSignal -): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); - -export const patchCaseConfigure = async ( - caseConfiguration: CasesConfigurePatch, - signal: AbortSignal -): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts deleted file mode 100644 index 85e472811c93b..0000000000000 --- 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 d2491b39fdf56..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Connector, - CasesConfigureResponse, - CasesConfigureRequest, -} from '../../../../../../../plugins/case/common/api'; -import { CaseConfigure } from './types'; - -export const connectorsMock: Connector[] = [ - { - id: '123', - actionTypeId: '.servicenow', - name: 'My Connector', - config: { - apiUrl: 'https://instance1.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - isPreconfigured: true, - }, - { - id: '456', - actionTypeId: '.servicenow', - name: 'My Connector 2', - config: { - apiUrl: 'https://instance2.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - isPreconfigured: true, - }, -]; - -export const caseConfigurationResposeMock: CasesConfigureResponse = { - created_at: '2020-04-06T13:03:18.657Z', - created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, - connector_id: '123', - connector_name: 'My Connector', - closure_type: 'close-by-user', - updated_at: '2020-04-06T14:03:18.657Z', - updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, - version: 'WzHJ12', -}; - -export const caseConfigurationMock: CasesConfigureRequest = { - connector_id: '123', - connector_name: 'My Connector', - closure_type: 'close-by-user', -}; - -export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { - createdAt: '2020-04-06T13:03:18.657Z', - createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, - connectorId: '123', - connectorName: 'My Connector', - closureType: 'close-by-user', - updatedAt: '2020-04-06T14:03:18.657Z', - updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, - version: 'WzHJ12', -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts deleted file mode 100644 index d69c23fe02ec9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ElasticUser } from '../types'; -import { - ActionType, - CasesConfigurationMaps, - CaseField, - ClosureType, - Connector, - ThirdPartyField, -} from '../../../../../../../plugins/case/common/api'; - -export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField }; - -export interface CasesConfigurationMapping { - source: CaseField; - target: ThirdPartyField; - actionType: ActionType; -} - -export interface CaseConfigure { - createdAt: string; - createdBy: ElasticUser; - connectorId: string; - connectorName: string; - closureType: ClosureType; - updatedAt: string; - updatedBy: ElasticUser; - version: string; -} - -export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { - actionType?: ActionType; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx deleted file mode 100644 index 3ee16e19eaf9f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { renderHook, act } from '@testing-library/react-hooks'; -import { useCaseConfigure, ReturnUseCaseConfigure, PersistCaseConfigure } from './use_configure'; -import { caseConfigurationCamelCaseResponseMock } from './mock'; -import * as api from './api'; - -jest.mock('./api'); - -const configuration: PersistCaseConfigure = { - connectorId: '456', - connectorName: 'My Connector 2', - closureType: 'close-by-pushing', -}; - -describe('useConfigure', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - const args = { - setConnector: jest.fn(), - setClosureType: jest.fn(), - setCurrentConfiguration: jest.fn(), - }; - - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: true, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('fetch case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('fetch case configuration - setConnector', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setConnector).toHaveBeenCalledWith('123', 'My Connector'); - }); - }); - - test('fetch case configuration - setClosureType', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setClosureType).toHaveBeenCalledWith('close-by-user'); - }); - }); - - test('fetch case configuration - setCurrentConfiguration', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setCurrentConfiguration).toHaveBeenCalledWith({ - connectorId: '123', - closureType: 'close-by-user', - }); - }); - }); - - test('fetch case configuration - only setConnector', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure({ setConnector: jest.fn() }) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('refetch case configuration', async () => { - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - result.current.refetchCaseConfigure(); - expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); - }); - }); - - test('set isLoading to true when fetching case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - result.current.refetchCaseConfigure(); - - expect(result.current.loading).toBe(true); - }); - }); - - test('persist case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - expect(result.current).toEqual({ - loading: false, - persistLoading: true, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('save case configuration - postCaseConfigure', async () => { - // When there is no version, a configuration is created. Otherwise is updated. - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - spyOnGetCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - version: '', - }) - ); - - const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); - spyOnPostCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - ...configuration, - }) - ); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); - expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); - expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { - connectorId: '456', - closureType: 'close-by-pushing', - }); - }); - }); - - test('save case configuration - patchCaseConfigure', async () => { - const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); - spyOnPatchCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - ...configuration, - }) - ); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); - expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); - expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { - connectorId: '456', - closureType: 'close-by-pushing', - }); - }); - }); - - test('save case configuration - only setConnector', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure({ setConnector: jest.fn() }) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('unhappy path - fetch case configuration', async () => { - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - spyOnGetCaseConfigure.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('unhappy path - persist case configuration', async () => { - const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); - spyOnPostCaseConfigure.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx deleted file mode 100644 index 1c03a09a8c2ea..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useState, useEffect, useCallback } from 'react'; -import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; - -import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; -import * as i18n from './translations'; -import { ClosureType } from './types'; -import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; - -export interface PersistCaseConfigure { - connectorId: string; - connectorName: string; - closureType: ClosureType; -} - -export interface ReturnUseCaseConfigure { - loading: boolean; - refetchCaseConfigure: () => void; - persistCaseConfigure: ({ - connectorId, - connectorName, - closureType, - }: PersistCaseConfigure) => unknown; - persistLoading: boolean; -} - -interface UseCaseConfigure { - setConnector: (newConnectorId: string, newConnectorName?: string) => void; - setClosureType?: (newClosureType: ClosureType) => void; - setCurrentConfiguration?: (configuration: CurrentConfiguration) => void; -} - -export const useCaseConfigure = ({ - setConnector, - setClosureType, - setCurrentConfiguration, -}: UseCaseConfigure): ReturnUseCaseConfigure => { - const [, dispatchToaster] = useStateToaster(); - const [loading, setLoading] = useState(true); - const [firstLoad, setFirstLoad] = useState(false); - const [persistLoading, setPersistLoading] = useState(false); - const [version, setVersion] = useState(''); - - const refetchCaseConfigure = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchCaseConfiguration = async () => { - try { - setLoading(true); - const res = await getCaseConfigure({ signal: abortCtrl.signal }); - if (!didCancel) { - if (res != null) { - setConnector(res.connectorId, res.connectorName); - if (setClosureType != null) { - setClosureType(res.closureType); - } - setVersion(res.version); - - if (!firstLoad) { - setFirstLoad(true); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - connectorId: res.connectorId, - closureType: res.closureType, - }); - } - } - } - setLoading(false); - } - } catch (error) { - if (!didCancel) { - setLoading(false); - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - } - } - }; - - fetchCaseConfiguration(); - - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, []); - - const persistCaseConfigure = useCallback( - async ({ connectorId, connectorName, closureType }: PersistCaseConfigure) => { - let didCancel = false; - const abortCtrl = new AbortController(); - const saveCaseConfiguration = async () => { - try { - setPersistLoading(true); - const connectorObj = { - connector_id: connectorId, - connector_name: connectorName, - closure_type: closureType, - }; - const res = - version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrl.signal) - : await patchCaseConfigure( - { - ...connectorObj, - version, - }, - abortCtrl.signal - ); - if (!didCancel) { - setConnector(res.connectorId); - if (setClosureType) { - setClosureType(res.closureType); - } - setVersion(res.version); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - connectorId: res.connectorId, - closureType: res.closureType, - }); - } - - displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); - setPersistLoading(false); - } - } catch (error) { - if (!didCancel) { - setPersistLoading(false); - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - } - } - }; - saveCaseConfiguration(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, - [version] - ); - - useEffect(() => { - refetchCaseConfigure(); - }, []); - - return { - loading, - refetchCaseConfigure, - persistCaseConfigure, - persistLoading, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts deleted file mode 100644 index 0bda75e5bc9e0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; - -import { - CommentResponse, - ServiceConnectorCaseResponse, - Status, - UserAction, - UserActionField, - CaseResponse, - CasesStatusResponse, - CaseUserActionsResponse, - CasesResponse, - CasesFindResponse, -} from '../../../../../../plugins/case/common/api/cases'; -import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; - -export const basicCaseId = 'basic-case-id'; -const basicCommentId = 'basic-comment-id'; -const basicCreatedAt = '2020-02-19T23:06:33.798Z'; -const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; -const laterTime = '2020-02-28T15:02:57.995Z'; -export const elasticUser = { - fullName: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', -}; - -export const tags: string[] = ['coke', 'pepsi']; - -export const basicComment: Comment = { - comment: 'Solve this fast!', - id: basicCommentId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - pushedAt: null, - pushedBy: null, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFc', -}; - -export const basicCase: Case = { - closedAt: null, - closedBy: null, - id: basicCaseId, - comments: [basicComment], - createdAt: basicCreatedAt, - createdBy: elasticUser, - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags, - title: 'Another horrible breach!!', - totalComment: 1, - updatedAt: basicUpdatedAt, - updatedBy: elasticUser, - version: 'WzQ3LDFd', -}; - -export const basicCasePost: Case = { - ...basicCase, - updatedAt: null, - updatedBy: null, -}; - -export const basicCommentPatch: Comment = { - ...basicComment, - updatedAt: basicUpdatedAt, - updatedBy: { - username: 'elastic', - }, -}; - -export const basicCaseCommentPatch = { - ...basicCase, - comments: [basicCommentPatch], -}; - -export const casesStatus: CasesStatus = { - countClosedCases: 130, - countOpenCases: 20, -}; - -const basicPush = { - connectorId: 'connector_id', - connectorName: 'connector name', - externalId: 'external_id', - externalTitle: 'external title', - externalUrl: 'basicPush.com', - pushedAt: basicUpdatedAt, - pushedBy: elasticUser, -}; - -export const pushedCase: Case = { - ...basicCase, - externalService: basicPush, -}; - -export const serviceConnector: ServiceConnectorCaseResponse = { - number: '123', - incidentId: '444', - pushedDate: basicUpdatedAt, - url: 'connector.com', - comments: [ - { - commentId: basicCommentId, - pushedDate: basicUpdatedAt, - }, - ], -}; - -const basicAction = { - actionAt: basicCreatedAt, - actionBy: elasticUser, - oldValue: null, - newValue: 'what a cool value', - caseId: basicCaseId, - commentId: null, -}; - -export const casePushParams = { - actionBy: elasticUser, - caseId: basicCaseId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - incidentId: null, - title: 'what a cool value', - commentId: null, - updatedAt: basicCreatedAt, - updatedBy: elasticUser, - description: 'nice', -}; -export const actionTypeExecutorResult = { - actionId: 'string', - status: 'ok', - data: serviceConnector, -}; - -export const cases: Case[] = [ - basicCase, - { ...pushedCase, id: '1', totalComment: 0, comments: [] }, - { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, - { ...basicCase, id: '3', totalComment: 0, comments: [] }, - { ...basicCase, id: '4', totalComment: 0, comments: [] }, -]; - -export const allCases: AllCases = { - cases, - page: 1, - perPage: 5, - total: 10, - ...casesStatus, -}; -export const actionLicenses: ActionLicense[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, -]; - -// Snake case for mock api responses -export const elasticUserSnake = { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', -}; -export const basicCommentSnake: CommentResponse = { - ...basicComment, - comment: 'Solve this fast!', - id: basicCommentId, - created_at: basicCreatedAt, - created_by: elasticUserSnake, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, -}; - -export const basicCaseSnake: CaseResponse = { - ...basicCase, - status: 'open' as Status, - closed_at: null, - closed_by: null, - comments: [basicCommentSnake], - created_at: basicCreatedAt, - created_by: elasticUserSnake, - external_service: null, - updated_at: basicUpdatedAt, - updated_by: elasticUserSnake, -}; - -export const casesStatusSnake: CasesStatusResponse = { - count_closed_cases: 130, - count_open_cases: 20, -}; - -export const pushSnake = { - connector_id: 'connector_id', - connector_name: 'connector name', - external_id: 'external_id', - external_title: 'external title', - external_url: 'basicPush.com', -}; -const basicPushSnake = { - ...pushSnake, - pushed_at: basicUpdatedAt, - pushed_by: elasticUserSnake, -}; -export const pushedCaseSnake = { - ...basicCaseSnake, - external_service: basicPushSnake, -}; - -export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; -export const respReporters = [ - { username: 'alexis', full_name: null, email: null }, - { username: 'kim', full_name: null, email: null }, - { username: 'maria', full_name: null, email: null }, - { username: 'steph', full_name: null, email: null }, -]; -export const casesSnake: CasesResponse = [ - basicCaseSnake, - { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, - { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, - { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, - { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, -]; - -export const allCasesSnake: CasesFindResponse = { - cases: casesSnake, - page: 1, - per_page: 5, - total: 10, - ...casesStatusSnake, -}; - -const basicActionSnake = { - action_at: basicCreatedAt, - action_by: elasticUserSnake, - old_value: null, - new_value: 'what a cool value', - case_id: basicCaseId, - comment_id: null, -}; -export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ - ...basicActionSnake, - action_id: `${af[0]}-${a}`, - action_field: af, - action: a, - comment_id: af[0] === 'comment' ? basicCommentId : null, - new_value: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); - -export const caseUserActionsSnake: CaseUserActionsResponse = [ - getUserActionSnake(['description'], 'create'), - getUserActionSnake(['comment'], 'create'), - getUserActionSnake(['description'], 'update'), -]; - -// user actions - -export const getUserAction = (af: UserActionField, a: UserAction) => ({ - ...basicAction, - actionId: `${af[0]}-${a}`, - actionField: af, - action: a, - commentId: af[0] === 'comment' ? basicCommentId : null, - newValue: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); - -export const caseUserActions: CaseUserActions[] = [ - getUserAction(['description'], 'create'), - getUserAction(['comment'], 'create'), - getUserAction(['description'], 'update'), -]; - -// components tests -export const useGetCasesMockState: UseGetCasesState = { - data: allCases, - loading: [], - selectedCases: [], - isError: false, - queryParams: DEFAULT_QUERY_PARAMS, - filterOptions: DEFAULT_FILTER_OPTIONS, -}; - -export const basicCaseClosed: Case = { - ...basicCase, - closedAt: '2020-02-25T23:06:33.798Z', - closedBy: elasticUser, - status: 'closed', -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts deleted file mode 100644 index e552f22b55fa4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { User, UserActionField, UserAction } from '../../../../../../plugins/case/common/api'; - -export interface Comment { - id: string; - createdAt: string; - createdBy: ElasticUser; - comment: string; - pushedAt: string | null; - pushedBy: string | null; - updatedAt: string | null; - updatedBy: ElasticUser | null; - version: string; -} -export interface CaseUserActions { - actionId: string; - actionField: UserActionField; - action: UserAction; - actionAt: string; - actionBy: ElasticUser; - caseId: string; - commentId: string | null; - newValue: string | null; - oldValue: string | null; -} - -export interface CaseExternalService { - pushedAt: string; - pushedBy: ElasticUser; - connectorId: string; - connectorName: string; - externalId: string; - externalTitle: string; - externalUrl: string; -} -export interface Case { - id: string; - closedAt: string | null; - closedBy: ElasticUser | null; - comments: Comment[]; - createdAt: string; - createdBy: ElasticUser; - description: string; - externalService: CaseExternalService | null; - status: string; - tags: string[]; - title: string; - totalComment: number; - updatedAt: string | null; - updatedBy: ElasticUser | null; - version: string; -} - -export interface QueryParams { - page: number; - perPage: number; - sortField: SortFieldCase; - sortOrder: 'asc' | 'desc'; -} - -export interface FilterOptions { - search: string; - status: string; - tags: string[]; - reporters: User[]; -} - -export interface CasesStatus { - countClosedCases: number | null; - countOpenCases: number | null; -} - -export interface AllCases extends CasesStatus { - cases: Case[]; - page: number; - perPage: number; - total: number; -} - -export enum SortFieldCase { - createdAt = 'createdAt', - closedAt = 'closedAt', -} - -export interface ElasticUser { - readonly email?: string | null; - readonly fullName?: string | null; - readonly username?: string | null; -} - -export interface FetchCasesProps extends ApiProps { - queryParams?: QueryParams; - filterOptions?: FilterOptions; -} - -export interface ApiProps { - signal: AbortSignal; -} - -export interface BulkUpdateStatus { - status: string; - id: string; - version: string; -} -export interface ActionLicense { - id: string; - name: string; - enabled: boolean; - enabledInConfig: boolean; - enabledInLicense: boolean; -} - -export interface DeleteCase { - id: string; - title?: string; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts deleted file mode 100644 index 1ec98bf5b5f1f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { camelCase, isArray, isObject, set } from 'lodash'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { - CasesFindResponse, - CasesFindResponseRt, - CaseResponse, - CaseResponseRt, - CasesResponse, - CasesResponseRt, - CasesStatusResponseRt, - CasesStatusResponse, - throwErrors, - CasesConfigureResponse, - CaseConfigureResponseRt, - CaseUserActionsResponse, - CaseUserActionsResponseRt, - ServiceConnectorCaseResponseRt, - ServiceConnectorCaseResponse, -} from '../../../../../../plugins/case/common/api'; -import { ToasterError } from '../../components/toasters'; -import { AllCases, Case } from './types'; - -export const getTypedPayload = <T>(a: unknown): T => a as T; - -export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => - arrayOfSnakes.reduce((acc: unknown[], value) => { - if (isArray(value)) { - return [...acc, convertArrayToCamelCase(value)]; - } else if (isObject(value)) { - return [...acc, convertToCamelCase(value)]; - } else { - return [...acc, value]; - } - }, []); - -export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U => - Object.entries(snakeCase).reduce((acc, [key, value]) => { - if (isArray(value)) { - set(acc, camelCase(key), convertArrayToCamelCase(value)); - } else if (isObject(value)) { - set(acc, camelCase(key), convertToCamelCase(value)); - } else { - set(acc, camelCase(key), value); - } - return acc; - }, {} as U); - -export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ - cases: snakeCases.cases.map(snakeCase => convertToCamelCase<CaseResponse, Case>(snakeCase)), - countClosedCases: snakeCases.count_closed_cases, - countOpenCases: snakeCases.count_open_cases, - page: snakeCases.page, - perPage: snakeCases.per_page, - total: snakeCases.total, -}); - -export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => - pipe( - CasesStatusResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const createToasterPlainError = (message: string) => new ToasterError([message]); - -export const decodeCaseResponse = (respCase?: CaseResponse) => - pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesResponse = (respCase?: CasesResponse) => - pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => - pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => - pipe( - CaseConfigureResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => - pipe( - CaseUserActionsResponseRt.decode(respUserActions), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => - pipe( - ServiceConnectorCaseResponseRt.decode(respPushCase), - fold(throwErrors(createToasterPlainError), identity) - ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts deleted file mode 100644 index 69f4c93a82e2c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS_URL, - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - DETECTION_ENGINE_TAGS_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { - AddRulesProps, - DeleteRulesProps, - DuplicateRulesProps, - EnableRulesProps, - FetchRulesProps, - FetchRulesResponse, - NewRule, - Rule, - FetchRuleProps, - BasicFetchProps, - ImportDataProps, - ExportDocumentsProps, - RuleStatusResponse, - ImportDataResponse, - PrePackagedRulesStatusResponse, - BulkRuleResponse, -} from './types'; -import { KibanaServices } from '../../../lib/kibana'; -import * as i18n from '../../../pages/detection_engine/rules/translations'; - -/** - * Add provided Rule - * - * @param rule to add - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => - KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { - method: rule.id != null ? 'PUT' : 'POST', - body: JSON.stringify(rule), - signal, - }); - -/** - * Fetches all rules from the Detection Engine API - * - * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) - * @param pagination desired pagination options (e.g. page/perPage) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRules = async ({ - filterOptions = { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: false, - tags: [], - }, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, - signal, -}: FetchRulesProps): Promise<FetchRulesResponse> => { - const filters = [ - ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), - ...(filterOptions.showCustomRules - ? [`alert.attributes.tags: "__internal_immutable:false"`] - : []), - ...(filterOptions.showElasticRules - ? [`alert.attributes.tags: "__internal_immutable:true"`] - : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), - ]; - - const query = { - page: pagination.page, - per_page: pagination.perPage, - sort_field: filterOptions.sortField, - sort_order: filterOptions.sortOrder, - ...(filters.length ? { filter: filters.join(' AND ') } : {}), - }; - - return KibanaServices.get().http.fetch<FetchRulesResponse>( - `${DETECTION_ENGINE_RULES_URL}/_find`, - { - method: 'GET', - query, - signal, - } - ); -}; - -/** - * Fetch a Rule by providing a Rule ID - * - * @param id Rule ID's (not rule_id) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => - KibanaServices.get().http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { - method: 'GET', - query: { id }, - signal, - }); - -/** - * Enables/Disables provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to enable/disable - * @param enabled to enable or disable - * - * @throws An error if response is not OK - */ -export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<BulkRuleResponse>(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { - method: 'PATCH', - body: JSON.stringify(ids.map(id => ({ id, enabled }))), - }); - -/** - * Deletes provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to delete - * - * @throws An error if response is not OK - */ -export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - body: JSON.stringify(ids.map(id => ({ id }))), - }); - -/** - * Duplicates provided Rules - * - * @param rules to duplicate - * - * @throws An error if response is not OK - */ -export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { - method: 'POST', - body: JSON.stringify( - rules.map(rule => ({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: undefined, - last_success_at: undefined, - last_success_message: undefined, - last_failure_at: undefined, - last_failure_message: undefined, - status: undefined, - status_date: undefined, - })) - ), - }); - -/** - * Create Prepackaged Rules - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { - await KibanaServices.get().http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { - method: 'PUT', - signal, - }); - - return true; -}; - -/** - * Imports rules in the same format as exported via the _export API - * - * @param fileToImport File to upload containing rules to import - * @param overwrite whether or not to overwrite rules with the same ruleId - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const importRules = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise<ImportDataResponse> => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch<ImportDataResponse>( - `${DETECTION_ENGINE_RULES_URL}/_import`, - { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - } - ); -}; - -/** - * Export rules from the server as a file download - * - * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) - * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) - * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const exportRules = async ({ - excludeExportDetails = false, - filename = `${i18n.EXPORT_FILENAME}.ndjson`, - ids = [], - signal, -}: ExportDocumentsProps): Promise<Blob> => { - const body = - ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; - - return KibanaServices.get().http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - }); -}; - -/** - * Get Rule Status provided Rule ID - * - * @param id string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRuleStatusById = async ({ - id, - signal, -}: { - id: string; - signal: AbortSignal; -}): Promise<RuleStatusResponse> => - KibanaServices.get().http.fetch<RuleStatusResponse>(DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ ids: [id] }), - signal, - }); - -/** - * Return rule statuses given list of alert ids - * - * @param ids array of string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRulesStatusByIds = async ({ - ids, - signal, -}: { - ids: string[]; - signal: AbortSignal; -}): Promise<RuleStatusResponse> => { - const res = await KibanaServices.get().http.fetch<RuleStatusResponse>( - DETECTION_ENGINE_RULES_STATUS_URL, - { - method: 'POST', - body: JSON.stringify({ ids }), - signal, - } - ); - return res; -}; - -/** - * Fetch all unique Tags used by Rules - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => - KibanaServices.get().http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { - method: 'GET', - signal, - }); - -/** - * Get pre packaged rules Status - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getPrePackagedRulesStatus = async ({ - signal, -}: { - signal: AbortSignal; -}): Promise<PrePackagedRulesStatusResponse> => - KibanaServices.get().http.fetch<PrePackagedRulesStatusResponse>( - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - { - method: 'GET', - signal, - } - ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts deleted file mode 100644 index 2f2de2e151664..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; - -import { RuleTypeSchema } from '../../../../../../../plugins/siem/common/detection_engine/types'; - -/** - * Params is an "record", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts - */ -export const action = t.exact( - t.type({ - group: t.string, - id: t.string, - action_type_id: t.string, - params: t.record(t.string, t.any), - }) -); - -export const NewRuleSchema = t.intersection([ - t.type({ - description: t.string, - enabled: t.boolean, - interval: t.string, - name: t.string, - risk_score: t.number, - severity: t.string, - type: RuleTypeSchema, - }), - t.partial({ - actions: t.array(action), - anomaly_threshold: t.number, - created_by: t.string, - false_positives: t.array(t.string), - filters: t.array(t.unknown), - from: t.string, - id: t.string, - index: t.array(t.string), - language: t.string, - machine_learning_job_id: t.string, - max_signals: t.number, - query: t.string, - references: t.array(t.string), - rule_id: t.string, - saved_id: t.string, - tags: t.array(t.string), - threat: t.array(t.unknown), - throttle: t.union([t.string, t.null]), - to: t.string, - updated_by: t.string, - note: t.string, - }), -]); - -export const NewRulesSchema = t.array(NewRuleSchema); -export type NewRule = t.TypeOf<typeof NewRuleSchema>; - -export interface AddRulesProps { - rule: NewRule; - signal: AbortSignal; -} - -const MetaRule = t.intersection([ - t.type({ - from: t.string, - }), - t.partial({ - throttle: t.string, - kibana_siem_app_url: t.string, - }), -]); - -export const RuleSchema = t.intersection([ - t.type({ - created_at: t.string, - created_by: t.string, - description: t.string, - enabled: t.boolean, - false_positives: t.array(t.string), - from: t.string, - id: t.string, - interval: t.string, - immutable: t.boolean, - name: t.string, - max_signals: t.number, - references: t.array(t.string), - risk_score: t.number, - rule_id: t.string, - severity: t.string, - tags: t.array(t.string), - type: RuleTypeSchema, - to: t.string, - threat: t.array(t.unknown), - updated_at: t.string, - updated_by: t.string, - actions: t.array(action), - throttle: t.union([t.string, t.null]), - }), - t.partial({ - anomaly_threshold: t.number, - filters: t.array(t.unknown), - index: t.array(t.string), - language: t.string, - last_failure_at: t.string, - last_failure_message: t.string, - meta: MetaRule, - machine_learning_job_id: t.string, - output_index: t.string, - query: t.string, - saved_id: t.string, - status: t.string, - status_date: t.string, - timeline_id: t.string, - timeline_title: t.string, - note: t.string, - version: t.number, - }), -]); - -export const RulesSchema = t.array(RuleSchema); - -export type Rule = t.TypeOf<typeof RuleSchema>; -export type Rules = t.TypeOf<typeof RulesSchema>; - -export interface RuleError { - id?: string; - rule_id?: string; - error: { status_code: number; message: string }; -} - -export type BulkRuleResponse = Array<Rule | RuleError>; - -export interface RuleResponseBuckets { - rules: Rule[]; - errors: RuleError[]; -} - -export interface PaginationOptions { - page: number; - perPage: number; - total: number; -} - -export interface FetchRulesProps { - pagination?: PaginationOptions; - filterOptions?: FilterOptions; - signal: AbortSignal; -} - -export interface FilterOptions { - filter: string; - sortField: string; - sortOrder: 'asc' | 'desc'; - showCustomRules?: boolean; - showElasticRules?: boolean; - tags?: string[]; -} - -export interface FetchRulesResponse { - page: number; - perPage: number; - total: number; - data: Rule[]; -} - -export interface FetchRuleProps { - id: string; - signal: AbortSignal; -} - -export interface EnableRulesProps { - ids: string[]; - enabled: boolean; -} - -export interface DeleteRulesProps { - ids: string[]; -} - -export interface DuplicateRulesProps { - rules: Rule[]; -} - -export interface BasicFetchProps { - signal: AbortSignal; -} - -export interface ImportDataProps { - fileToImport: File; - overwrite?: boolean; - signal: AbortSignal; -} - -export interface ImportRulesResponseError { - rule_id: string; - error: { - status_code: number; - message: string; - }; -} - -export interface ImportDataResponse { - success: boolean; - success_count: number; - errors: ImportRulesResponseError[]; -} - -export interface ExportDocumentsProps { - ids: string[]; - filename?: string; - excludeExportDetails?: boolean; - signal: AbortSignal; -} - -export interface RuleStatus { - current_status: RuleInfoStatus; - failures: RuleInfoStatus[]; -} - -export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; -export interface RuleInfoStatus { - alert_id: string; - status_date: string; - status: RuleStatusType | null; - last_failure_at: string | null; - last_success_at: string | null; - last_failure_message: string | null; - last_success_message: string | null; - last_look_back_date: string | null | undefined; - gap: string | null | undefined; - bulk_create_time_durations: string[] | null | undefined; - search_after_time_durations: string[] | null | undefined; -} - -export type RuleStatusResponse = Record<string, RuleStatus>; - -export interface PrePackagedRulesStatusResponse { - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts deleted file mode 100644 index ece2483adde3a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - DETECTION_ENGINE_QUERY_SIGNALS_URL, - DETECTION_ENGINE_SIGNALS_STATUS_URL, - DETECTION_ENGINE_INDEX_URL, - DETECTION_ENGINE_PRIVILEGES_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { KibanaServices } from '../../../lib/kibana'; -import { - BasicSignals, - Privilege, - QuerySignals, - SignalSearchResponse, - SignalsIndex, - UpdateSignalStatusProps, -} from './types'; - -/** - * Fetch Signals by providing a query - * - * @param query String to match a dsl - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchQuerySignals = async <Hit, Aggregations>({ - query, - signal, -}: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => - KibanaServices.get().http.fetch<SignalSearchResponse<Hit, Aggregations>>( - DETECTION_ENGINE_QUERY_SIGNALS_URL, - { - method: 'POST', - body: JSON.stringify(query), - signal, - } - ); - -/** - * Update signal status by query - * - * @param query of signals to update - * @param status to update to('open' / 'closed') - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const updateSignalStatus = async ({ - query, - status, - signal, -}: UpdateSignalStatusProps): Promise<unknown> => - KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ status, ...query }), - signal, - }); - -/** - * Fetch Signal Index - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => - KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { - method: 'GET', - signal, - }); - -/** - * Get User Privileges - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => - KibanaServices.get().http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { - method: 'GET', - signal, - }); - -/** - * Create Signal Index if needed it - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => - KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { - method: 'POST', - signal, - }); diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts deleted file mode 100644 index 8628ba502f081..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; -import { useApolloClient } from '../../../utils/apollo_context'; - -export interface LastEventTimeArgs { - id: string; - errorMessage: string; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: LastEventTimeArgs) => React.ReactNode; - indexKey: LastEventIndexKey; -} - -export function useLastEventTimeQuery<TCache = object>( - indexKey: LastEventIndexKey, - details: LastTimeDetails, - sourceId: string -) { - const [loading, updateLoading] = useState(false); - const [lastSeen, updateLastSeen] = useState<number | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - const [currentIndexKey, updateCurrentIndexKey] = useState<LastEventIndexKey | null>(null); - const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const apolloClient = useApolloClient(); - async function fetchLastEventTime(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query<GetLastEventTimeQuery.Query, GetLastEventTimeQuery.Variables>({ - query: LastEventTimeGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - indexKey, - details, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); - updateErrorMessage(null); - updateCurrentIndexKey(currentIndexKey); - }, - error => { - updateLoading(false); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchLastEventTime(signal); - return () => abortCtrl.abort(); - }, [apolloClient, indexKey, details.hostName, details.ip]); - - return { lastSeen, loading, errorMessage }; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts deleted file mode 100644 index 5ef8e67dedddb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; - -interface MockLastEventTimeQuery { - request: { - query: GetLastEventTimeQuery.Query; - variables: GetLastEventTimeQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - LastEventTime: { - lastSeen: string | null; - errorMessage: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} - -const getTimeTwelveMinutesAgo = () => { - const d = new Date(); - const ts = d.getTime(); - const twelveMinutes = ts - 12 * 60 * 1000; - return new Date(twelveMinutes).toISOString(); -}; - -export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ - { - request: { - query: LastEventTimeGqlQuery, - variables: { - sourceId: 'default', - indexKey: LastEventIndexKey.hosts, - details: {}, - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - LastEventTime: { - lastSeen: getTimeTwelveMinutesAgo(), - errorMessage: null, - }, - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts deleted file mode 100644 index 67cfe259927ab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; - -import { createFilter } from './helpers'; - -describe('Helpers', () => { - describe('#createFilter', () => { - test('if it is a string it returns untouched', () => { - const filter = createFilter('even invalid strings return the same'); - expect(filter).toBe('even invalid strings return the same'); - }); - - test('if it is an ESQuery object it will be returned as a string', () => { - const query: ESQuery = { term: { 'host.id': 'host-value' } }; - const filter = createFilter(query); - expect(filter).toBe(JSON.stringify(query)); - }); - - test('if it is undefined, then undefined is returned', () => { - const filter = createFilter(undefined); - expect(filter).toBe(undefined); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.ts deleted file mode 100644 index 7ff9577bfb05e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FetchPolicy } from 'apollo-client'; -import { isString } from 'lodash/fp'; - -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; - -export const createFilter = (filterQuery: ESQuery | string | undefined) => - isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); - -export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts deleted file mode 100644 index 5806125f2397b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ApolloClient from 'apollo-client'; -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -export interface FirstLastSeenHostArgs { - id: string; - errorMessage: string; - firstSeen: Date; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: FirstLastSeenHostArgs) => React.ReactNode; - hostName: string; -} - -export function useFirstLastSeenHostQuery<TCache = object>( - hostName: string, - sourceId: string, - apolloClient: ApolloClient<TCache> -) { - const [loading, updateLoading] = useState(false); - const [firstSeen, updateFirstSeen] = useState<Date | null>(null); - const [lastSeen, updateLastSeen] = useState<Date | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - - async function fetchFirstLastSeenHost(signal: AbortSignal) { - updateLoading(true); - return apolloClient - .query<GetHostFirstLastSeenQuery.Query, GetHostFirstLastSeenQuery.Variables>({ - query: HostFirstLastSeenGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - hostName, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); - updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); - updateErrorMessage(null); - }, - error => { - updateLoading(false); - updateFirstSeen(null); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchFirstLastSeenHost(signal); - return () => abortCtrl.abort(); - }, []); - - return { firstSeen, lastSeen, loading, errorMessage }; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts deleted file mode 100644 index 7376f38ae8d0f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -interface MockedProvidedQuery { - request: { - query: GetHostFirstLastSeenQuery.Query; - variables: GetHostFirstLastSeenQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - HostFirstLastSeen: { - firstSeen: string | null; - lastSeen: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} -export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ - { - request: { - query: HostFirstLastSeenGqlQuery, - variables: { - sourceId: 'default', - hostName: 'kibana-siem', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - HostFirstLastSeen: { - firstSeen: '2019-04-08T16:09:40.692Z', - lastSeen: '2019-04-08T18:35:45.064Z', - }, - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx deleted file mode 100644 index edf3f6855f955..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - Direction, - GetHostsTableQuery, - HostsEdges, - HostsFields, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; - -import { HostsTableQuery } from './hosts_table.gql_query'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; - -const ID = 'hostsQuery'; - -export interface HostsArgs { - endDate: number; - hosts: HostsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - startDate: number; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: HostsArgs) => React.ReactNode; - type: hostsModel.HostsType; - startDate: number; - endDate: number; -} - -export interface HostsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sortField: HostsFields; - direction: Direction; -} - -type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; - -class HostsComponentQuery extends QueryTemplatePaginated< - HostsProps, - GetHostsTableQuery.Query, - GetHostsTableQuery.Variables -> { - private memoizedHosts: ( - variables: string, - data: GetHostsTableQuery.Source | undefined - ) => HostsEdges[]; - - constructor(props: HostsProps) { - super(props); - this.memoizedHosts = memoizeOne(this.getHosts); - } - - public render() { - const { - activePage, - id = ID, - isInspected, - children, - direction, - filterQuery, - endDate, - kibana, - limit, - startDate, - skip, - sourceId, - sortField, - } = this.props; - const defaultIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); - - const variables: GetHostsTableQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: { - direction, - field: sortField, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex, - inspect: isInspected, - }; - return ( - <Query<GetHostsTableQuery.Query, GetHostsTableQuery.Variables> - query={HostsTableQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={variables} - skip={skip} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Hosts: { - ...fetchMoreResult.source.Hosts, - edges: [...fetchMoreResult.source.Hosts.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - endDate, - hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), - id, - inspect: getOr(null, 'source.Hosts.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Hosts.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - startDate, - totalCount: getOr(-1, 'source.Hosts.totalCount', data), - }); - }} - </Query> - ); - } - - private getHosts = ( - variables: string, - source: GetHostsTableQuery.Source | undefined - ): HostsEdges[] => getOr([], 'Hosts.edges', source); -} - -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHostsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(HostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx deleted file mode 100644 index 405c45348b54d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { inputsModel, inputsSelectors, State } from '../../../store'; -import { getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../../query_template'; -import { withKibana, WithKibanaProps } from '../../../lib/kibana'; - -import { HostOverviewQuery } from './host_overview.gql_query'; -import { GetHostOverviewQuery, HostItem } from '../../../graphql/types'; - -const ID = 'hostOverviewQuery'; - -export interface HostOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - hostOverview: HostItem; - loading: boolean; - refetch: inputsModel.Refetch; - startDate: number; - endDate: number; -} - -export interface HostOverviewReduxProps { - isInspected: boolean; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: HostOverviewArgs) => React.ReactNode; - hostName: string; - startDate: number; - endDate: number; -} - -type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; - -class HostOverviewByNameComponentQuery extends QueryTemplate< - HostsOverViewProps, - GetHostOverviewQuery.Query, - GetHostOverviewQuery.Variables -> { - public render() { - const { - id = ID, - isInspected, - children, - hostName, - kibana, - skip, - sourceId, - startDate, - endDate, - } = this.props; - return ( - <Query<GetHostOverviewQuery.Query, GetHostOverviewQuery.Variables> - query={HostOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - hostName, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const hostOverview = getOr([], 'source.HostOverview', data); - return children({ - id, - inspect: getOr(null, 'source.HostOverview.inspect', data), - refetch, - loading, - hostOverview, - startDate, - endDate, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostOverviewByNameQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(HostOverviewByNameComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx deleted file mode 100644 index 954bfede07139..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; -import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { ipOverviewQuery } from './index.gql_query'; - -const ID = 'ipOverviewQuery'; - -export interface IpOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - ipOverviewData: IpOverviewData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface IpOverviewProps extends QueryTemplateProps { - children: (args: IpOverviewArgs) => React.ReactNode; - type: networkModel.NetworkType; - ip: string; -} - -const IpOverviewComponentQuery = React.memo<IpOverviewProps & PropsFromRedux>( - ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( - <Query<GetIpOverviewQuery.Query, GetIpOverviewQuery.Variables> - query={ipOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - filterQuery: createFilter(filterQuery), - ip, - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const init: IpOverviewData = { host: {} }; - const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); - return children({ - id, - inspect: getOr(null, 'source.IpOverview.inspect', data), - ipOverviewData, - loading, - refetch, - }); - }} - </Query> - ) -); - -IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx deleted file mode 100644 index 3933aefa60483..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostDetailsQuery } from './index.gql_query'; - -const ID = 'kpiHostDetailsQuery'; - -export interface KpiHostDetailsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHostDetails: KpiHostDetailsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface QueryKpiHostDetailsProps extends QueryTemplateProps { - children: (args: KpiHostDetailsArgs) => React.ReactNode; -} - -const KpiHostDetailsComponentQuery = React.memo<QueryKpiHostDetailsProps & PropsFromRedux>( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - <Query<GetKpiHostDetailsQuery.Query, GetKpiHostDetailsQuery.Variables> - query={kpiHostDetailsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHostDetails.inspect', data), - kpiHostDetails, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx deleted file mode 100644 index 7035d63193118..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostsQuery } from './index.gql_query'; - -const ID = 'kpiHostsQuery'; - -export interface KpiHostsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHosts: KpiHostsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiHostsProps extends QueryTemplateProps { - children: (args: KpiHostsArgs) => React.ReactNode; -} - -const KpiHostsComponentQuery = React.memo<KpiHostsProps & PropsFromRedux>( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - <Query<GetKpiHostsQuery.Query, GetKpiHostsQuery.Variables> - query={kpiHostsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHosts = getOr({}, `source.KpiHosts`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHosts.inspect', data), - kpiHosts, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx deleted file mode 100644 index 002a819417df6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiNetworkQuery } from './index.gql_query'; - -const ID = 'kpiNetworkQuery'; - -export interface KpiNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiNetwork: KpiNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiNetworkProps extends QueryTemplateProps { - children: (args: KpiNetworkArgs) => React.ReactNode; -} - -const KpiNetworkComponentQuery = React.memo<KpiNetworkProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( - <Query<GetKpiNetworkQuery.Query, GetKpiNetworkQuery.Variables> - query={kpiNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiNetwork = getOr({}, `source.KpiNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiNetwork.inspect', data), - kpiNetwork, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx deleted file mode 100644 index af4eb1ff7a5e1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { QuerySuggestion, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { useKibana } from '../../lib/kibana'; - -type RendererResult = React.ReactElement<JSX.Element> | null; -type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result; - -interface KueryAutocompletionLifecycleProps { - children: RendererFunction<{ - isLoadingSuggestions: boolean; - loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: QuerySuggestion[]; - }>; - indexPattern: IIndexPattern; -} - -interface KueryAutocompletionCurrentRequest { - expression: string; - cursorPosition: number; -} - -export const KueryAutocompletion = React.memo<KueryAutocompletionLifecycleProps>( - ({ children, indexPattern }) => { - const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>( - null - ); - const [suggestions, setSuggestions] = useState<QuerySuggestion[]>([]); - const kibana = useKibana(); - const loadSuggestions = async ( - expression: string, - cursorPosition: number, - maxSuggestions?: number - ) => { - const language = 'kuery'; - - if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { - return; - } - - const futureRequest = { - expression, - cursorPosition, - }; - setCurrentRequest({ - expression, - cursorPosition, - }); - setSuggestions([]); - - if ( - futureRequest && - futureRequest.expression !== (currentRequest && currentRequest.expression) && - futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) - ) { - const newSuggestions = - (await kibana.services.data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter: [], - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - })) || []; - - setCurrentRequest(null); - setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); - } - }; - - return children({ - isLoadingSuggestions: currentRequest !== null, - loadSuggestions, - suggestions, - }); - } -); - -KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts deleted file mode 100644 index 55d7e7cdc6e54..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState, useRef } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { useUiSetting$ } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { useApolloClient } from '../../utils/apollo_context'; -import { inputsModel } from '../../store'; -import { MatrixHistogramGqlQuery } from './index.gql_query'; -import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; - -export const useQuery = <Hit, Aggs, TCache = object>({ - endDate, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, -}: MatrixHistogramQueryProps) => { - const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo<string[]>(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - const [, dispatchToaster] = useStateToaster(); - const refetch = useRef<inputsModel.Refetch>(); - const [loading, setLoading] = useState<boolean>(false); - const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); - const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); - const [totalCount, setTotalCount] = useState<number>(-1); - const apolloClient = useApolloClient(); - - useEffect(() => { - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, - }; - let isSubscribed = true; - const abortCtrl = new AbortController(); - const abortSignal = abortCtrl.signal; - - async function fetchData() { - if (!apolloClient) return null; - setLoading(true); - return apolloClient - .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ - query: MatrixHistogramGqlQuery, - fetchPolicy: 'network-only', - variables: matrixHistogramVariables, - context: { - fetchOptions: { - abortSignal, - }, - }, - }) - .then( - result => { - if (isSubscribed) { - const source = result?.data?.source?.MatrixHistogram ?? {}; - setData(source?.matrixHistogramData ?? []); - setTotalCount(source?.totalCount ?? -1); - setInspect(source?.inspect ?? null); - setLoading(false); - } - }, - error => { - if (isSubscribed) { - setData(null); - setTotalCount(-1); - setInspect(null); - setLoading(false); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - ); - } - refetch.current = fetchData; - fetchData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [ - defaultIndex, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - data, - ]); - - return { data, loading, inspect, totalCount, refetch: refetch.current }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx deleted file mode 100644 index 060b66fc3cbbe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DocumentNode } from 'graphql'; -import { ScaleType } from '@elastic/charts'; -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkDnsQuery } from './index.gql_query'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; -import { MatrixHistogram } from '../../components/matrix_histogram'; -import { MatrixHistogramOption, GetSubTitle } from '../../components/matrix_histogram/types'; -import { UpdateDateRange } from '../../components/charts/common'; -import { SetQuery } from '../../pages/hosts/navigation/types'; - -const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; -export interface NetworkDnsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkDns: NetworkDnsEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - stackByField?: string; - totalCount: number; - histogram: MatrixOverOrdinalHistogramData[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; - type: networkModel.NetworkType; -} - -interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - isDnsHistogram?: boolean; - query: DocumentNode; - scaleType: ScaleType; - setQuery: SetQuery; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string; - type: networkModel.NetworkType; - updateDateRange: UpdateDateRange; - yTickFormatter?: (value: number) => string; -} - -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} - -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - <Query<GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -const makeMapHistogramStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -export const NetworkDnsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); - -export const NetworkDnsHistogramQuery = compose<React.ComponentClass<DnsHistogramOwnProps>>( - connect(makeMapHistogramStateToProps), - withKibana -)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx deleted file mode 100644 index b13637fa88d07..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetNetworkHttpQuery, - NetworkHttpEdges, - NetworkHttpSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkHttpQuery } from './index.gql_query'; - -const ID = 'networkHttpQuery'; - -export interface NetworkHttpArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkHttp: NetworkHttpEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkHttpArgs) => React.ReactNode; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkHttpComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkHttpSortField; -} - -type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; - -class NetworkHttpComponentQuery extends QueryTemplatePaginated< - NetworkHttpProps, - GetNetworkHttpQuery.Query, - GetNetworkHttpQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - sort, - startDate, - } = this.props; - const variables: GetNetworkHttpQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkHttpQuery.Query, GetNetworkHttpQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkHttpQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkHttp = getOr([], `source.NetworkHttp.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkHttp: { - ...fetchMoreResult.source.NetworkHttp, - edges: [...fetchMoreResult.source.NetworkHttp.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkHttp.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkHttp, - pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getHttpSelector = networkSelectors.httpSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHttpSelector(state, type), - isInspected, - }; - }; -}; - -export const NetworkHttpQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkHttpComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx deleted file mode 100644 index 17a14ce3a1120..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopCountriesQuery, - NetworkTopCountriesEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopCountriesQuery } from './index.gql_query'; - -const ID = 'networkTopCountriesQuery'; - -export interface NetworkTopCountriesArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopCountries: NetworkTopCountriesEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopCountriesArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopCountriesComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; - -class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< - NetworkTopCountriesProps, - GetNetworkTopCountriesQuery.Query, - GetNetworkTopCountriesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopCountriesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkTopCountriesQuery.Query, GetNetworkTopCountriesQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopCountriesQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopCountries: { - ...fetchMoreResult.source.NetworkTopCountries, - edges: [...fetchMoreResult.source.NetworkTopCountries.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopCountries, - pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopCountriesSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopCountriesQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx deleted file mode 100644 index fdac282292a4b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopNFlowQuery, - NetworkTopNFlowEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopNFlowQuery } from './index.gql_query'; - -const ID = 'networkTopNFlowQuery'; - -export interface NetworkTopNFlowArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopNFlow: NetworkTopNFlowEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopNFlowArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopNFlowComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; - -class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< - NetworkTopNFlowProps, - GetNetworkTopNFlowQuery.Query, - GetNetworkTopNFlowQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopNFlowQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkTopNFlowQuery.Query, GetNetworkTopNFlowQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopNFlowQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopNFlow: { - ...fetchMoreResult.source.NetworkTopNFlow, - edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopNFlow, - pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopNFlowSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopNFlowQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx deleted file mode 100644 index e7b68bf557a21..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { State } from '../../../store'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewHostQuery } from './index.gql_query'; - -export const ID = 'overviewHostQuery'; - -export interface OverviewHostArgs { - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - overviewHost: OverviewHostData; - refetch: inputsModel.Refetch; -} - -export interface OverviewHostProps extends QueryTemplateProps { - children: (args: OverviewHostArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -const OverviewHostComponentQuery = React.memo<OverviewHostProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { - return ( - <Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables> - query={overviewHostQuery} - fetchPolicy={getDefaultFetchPolicy()} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewHost = getOr({}, `source.OverviewHost`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewHost.inspect', data), - overviewHost, - loading, - refetch, - }); - }} - </Query> - ); - } -); - -OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx deleted file mode 100644 index c7f72ac6193f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { State } from '../../../store'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewNetworkQuery } from './index.gql_query'; - -export const ID = 'overviewNetworkQuery'; - -export interface OverviewNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - overviewNetwork: OverviewNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OverviewNetworkProps extends QueryTemplateProps { - children: (args: OverviewNetworkArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -export const OverviewNetworkComponentQuery = React.memo<OverviewNetworkProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( - <Query<GetOverviewNetworkQuery.Query, GetOverviewNetworkQuery.Variables> - query={overviewNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewNetwork.inspect', data), - overviewNetwork, - loading, - refetch, - }); - }} - </Query> - ) -); - -OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx deleted file mode 100644 index 3467e2b5f18d8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isUndefined } from 'lodash'; -import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; -import { Query } from 'react-apollo'; -import React, { useEffect, useMemo, useState } from 'react'; -import memoizeOne from 'memoize-one'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -import { IndexField, SourceQuery } from '../../graphql/types'; - -import { sourceQuery } from './index.gql_query'; -import { useApolloClient } from '../../utils/apollo_context'; - -export { sourceQuery }; - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly<Record<string, Partial<BrowserField>>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; -} - -export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>; - -export const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> => - Object.values(browserFields).reduce<Array<Partial<BrowserField>>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -export const getAllFieldsByName = ( - browserFields: BrowserFields -): { [fieldName: string]: Partial<BrowserField> } => - keyBy('name', getAllBrowserFields(browserFields)); - -interface WithSourceArgs { - indicesExist: boolean; - browserFields: BrowserFields; - indexPattern: IIndexPattern; -} - -interface WithSourceProps { - children: (args: WithSourceArgs) => React.ReactNode; - indexToAdd?: string[] | null; - sourceId: string; -} - -export const getIndexFields = memoizeOne( - (title: string, fields: IndexField[]): IIndexPattern => - fields && fields.length > 0 - ? { - fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), - title, - } - : { fields: [], title } -); - -export const getBrowserFields = memoizeOne( - (title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce<BrowserFields>( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {} -); - -export const WithSource = React.memo<WithSourceProps>(({ children, indexToAdd, sourceId }) => { - const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo<string[]>(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - return ( - <Query<SourceQuery.Query, SourceQuery.Variables> - query={sourceQuery} - fetchPolicy="cache-first" - notifyOnNetworkStatusChange - variables={{ - sourceId, - defaultIndex, - }} - > - {({ data }) => - children({ - indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields( - defaultIndex.join(), - get('source.status.indexFields', data) - ), - indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), - }) - } - </Query> - ); -}); - -WithSource.displayName = 'WithSource'; - -export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => - indicesExist || isUndefined(indicesExist); - -export const useWithSource = (sourceId: string, indices: string[]) => { - const [loading, updateLoading] = useState(false); - const [indicesExist, setIndicesExist] = useState<boolean | undefined | null>(undefined); - const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null); - const [indexPattern, setIndexPattern] = useState<IIndexPattern | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - - const apolloClient = useApolloClient(); - async function fetchSource(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query<SourceQuery.Query, SourceQuery.Variables>({ - query: sourceQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - defaultIndex: indices, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateErrorMessage(null); - setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setIndexPattern( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - }, - error => { - updateLoading(false); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchSource(signal); - return () => abortCtrl.abort(); - }, [apolloClient, sourceId, indices]); - - return { indicesExist, browserFields, indexPattern, loading, errorMessage }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts b/x-pack/legacy/plugins/siem/public/containers/source/mock.ts deleted file mode 100644 index 805c69f7fcc12..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../../../../plugins/siem/common/constants'; - -import { BrowserFields } from '.'; -import { sourceQuery } from './index.gql_query'; - -export const mocksSource = [ - { - request: { - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - configuration: {}, - status: { - indicesExist: true, - winlogbeatIndices: [ - 'winlogbeat-7.0.0-2019.02.17', - 'winlogbeat-7.0.0-2019.02.18', - 'winlogbeat-7.0.0-2019.02.19', - 'winlogbeat-7.0.0-2019.02.20', - 'winlogbeat-7.0.0-2019.02.21', - 'winlogbeat-7.0.0-2019.02.21-000001', - 'winlogbeat-7.0.0-2019.02.22', - 'winlogbeat-8.0.0-2019.02.19-000001', - ], - auditbeatIndices: [ - 'auditbeat-7.0.0-2019.02.17', - 'auditbeat-7.0.0-2019.02.18', - 'auditbeat-7.0.0-2019.02.19', - 'auditbeat-7.0.0-2019.02.20', - 'auditbeat-7.0.0-2019.02.21', - 'auditbeat-7.0.0-2019.02.21-000001', - 'auditbeat-7.0.0-2019.02.22', - 'auditbeat-8.0.0-2019.02.19-000001', - ], - filebeatIndices: [ - 'filebeat-7.0.0-iot-2019.06', - 'filebeat-7.0.0-iot-2019.07', - 'filebeat-7.0.0-iot-2019.08', - 'filebeat-7.0.0-iot-2019.09', - 'filebeat-7.0.0-iot-2019.10', - 'filebeat-8.0.0-2019.02.19-000001', - ], - indexFields: [ - { - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'source', - description: - 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - }, - ], - }, - }, - }, - }, - }, -]; - -export const mockIndexFields = [ - { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, - { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, -]; - -export const mockBrowserFields: BrowserFields = { - agent: { - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'agent.id': { - aggregatable: true, - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - }, - 'agent.name': { - aggregatable: true, - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - }, - }, - }, - auditd: { - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - 'auditd.data.a1': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - }, - 'auditd.data.a2': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - }, - }, - }, - base: { - fields: { - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - }, - }, - client: { - fields: { - 'client.address': { - aggregatable: true, - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - }, - 'client.bytes': { - aggregatable: true, - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - aggregatable: true, - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - }, - 'cloud.availability_zone': { - aggregatable: true, - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - }, - }, - }, - container: { - fields: { - 'container.id': { - aggregatable: true, - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - }, - 'container.image.name': { - aggregatable: true, - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - }, - 'container.image.tag': { - aggregatable: true, - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - }, - }, - }, - destination: { - fields: { - 'destination.address': { - aggregatable: true, - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - }, - 'destination.bytes': { - aggregatable: true, - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - }, - 'destination.domain': { - aggregatable: true, - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - }, - 'destination.ip': { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - 'destination.port': { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - }, - }, - event: { - fields: { - 'event.end': { - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - aggregatable: true, - }, - }, - }, - source: { - fields: { - 'source.ip': { - aggregatable: true, - category: 'source', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - 'source.port': { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts deleted file mode 100644 index 32ac62d594e1c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - TIMELINE_IMPORT_URL, - TIMELINE_EXPORT_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules'; -import { KibanaServices } from '../../../lib/kibana'; -import { ExportSelectedData } from '../../../components/generic_downloader'; - -export const importTimelines = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise<ImportDataResponse> => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - }); -}; - -export const exportSelectedTimeline: ExportSelectedData = async ({ - excludeExportDetails = false, - filename = `timelines_export.ndjson`, - ids = [], - signal, -}): Promise<Blob> => { - const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - asResponse: true, - }); - - return response.body!; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx deleted file mode 100644 index b5c91ca287f0b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useCallback } from 'react'; -import { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; - -import { Query } from 'react-apollo'; - -import { ApolloQueryResult } from 'apollo-client'; -import { OpenTimelineResult } from '../../../components/open_timeline/types'; -import { - GetAllTimeline, - PageInfoTimeline, - SortTimeline, - TimelineResult, -} from '../../../graphql/types'; -import { allTimelinesQuery } from './index.gql_query'; - -export interface AllTimelinesArgs { - timelines: OpenTimelineResult[]; - loading: boolean; - totalCount: number; - refetch: () => void; -} - -export interface AllTimelinesVariables { - onlyUserFavorite: boolean; - pageInfo: PageInfoTimeline; - search: string; - sort: SortTimeline; -} - -interface OwnProps extends AllTimelinesVariables { - children?: (args: AllTimelinesArgs) => React.ReactNode; -} - -type Refetch = ( - variables: GetAllTimeline.Variables | undefined -) => Promise<ApolloQueryResult<GetAllTimeline.Query>>; - -const getAllTimeline = memoizeOne( - (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => - timelines.map(timeline => ({ - created: timeline.created, - description: timeline.description, - eventIdToNoteIds: - timeline.eventIdToNoteIds != null - ? timeline.eventIdToNoteIds.reduce((acc, note) => { - if (note.eventId != null) { - const notes = getOr([], note.eventId, acc); - return { ...acc, [note.eventId]: [...notes, note.noteId] }; - } - return acc; - }, {}) - : null, - favorite: timeline.favorite, - noteIds: timeline.noteIds, - notes: - timeline.notes != null - ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) - : null, - pinnedEventIds: - timeline.pinnedEventIds != null - ? timeline.pinnedEventIds.reduce( - (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), - {} - ) - : null, - savedObjectId: timeline.savedObjectId, - title: timeline.title, - updated: timeline.updated, - updatedBy: timeline.updatedBy, - })) -); - -const AllTimelinesQueryComponent: React.FC<OwnProps> = ({ - children, - onlyUserFavorite, - pageInfo, - search, - sort, -}) => { - const variables: GetAllTimeline.Variables = { - onlyUserFavorite, - pageInfo, - search, - sort, - }; - const handleRefetch = useCallback((refetch: Refetch) => refetch(variables), [variables]); - - return ( - <Query<GetAllTimeline.Query, GetAllTimeline.Variables> - query={allTimelinesQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - refetch: handleRefetch.bind(null, refetch), - totalCount: getOr(0, 'getAllTimeline.totalCount', data), - timelines: getAllTimeline( - JSON.stringify(variables), - getOr([], 'getAllTimeline.timeline', data) - ), - }) - } - </Query> - ); -}; - -export const AllTimelinesQuery = React.memo(AllTimelinesQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx deleted file mode 100644 index 0debed9c5f9aa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; - -import { timelineDetailsQuery } from './index.gql_query'; - -export interface EventsArgs { - detailsData: DetailItem[] | null; - loading: boolean; -} - -export interface TimelineDetailsProps { - children?: (args: EventsArgs) => React.ReactElement; - indexName: string; - eventId: string; - executeQuery: boolean; - sourceId: string; -} - -const getDetailsEvent = memoizeOne( - (variables: string, detail: DetailItem[]): DetailItem[] => detail -); - -const TimelineDetailsQueryComponent: React.FC<TimelineDetailsProps> = ({ - children, - indexName, - eventId, - executeQuery, - sourceId, -}) => { - const variables: GetTimelineDetailsQuery.Variables = { - sourceId, - indexName, - eventId, - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - }; - return executeQuery ? ( - <Query<GetTimelineDetailsQuery.Query, GetTimelineDetailsQuery.Variables> - query={timelineDetailsQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - detailsData: getDetailsEvent( - JSON.stringify(variables), - getOr([], 'source.TimelineDetails.data', data) - ), - }) - } - </Query> - ) : ( - children!({ loading: false, detailsData: null }) - ); -}; - -export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx deleted file mode 100644 index 3c089ef6926dd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr, uniqBy } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { compose, Dispatch } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { - GetTimelineQuery, - PageInfo, - SortField, - TimelineEdges, - TimelineItem, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../query_template'; -import { EventType } from '../../store/timeline/model'; -import { timelineQuery } from './index.gql_query'; -import { timelineActions } from '../../store/timeline'; -import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals'; - -export interface TimelineArgs { - events: TimelineItem[]; - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - loadMore: (cursor: string, tieBreaker: string) => void; - pageInfo: PageInfo; - refetch: inputsModel.Refetch; - totalCount: number; - getUpdatedAt: () => number; -} - -export interface CustomReduxProps { - clearSignalsState: ({ id }: { id?: string }) => void; -} - -export interface OwnProps extends QueryTemplateProps { - children?: (args: TimelineArgs) => React.ReactNode; - eventType?: EventType; - id: string; - indexPattern?: IIndexPattern; - indexToAdd?: string[]; - limit: number; - sortField: SortField; - fields: string[]; -} - -type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; - -class TimelineQueryComponent extends QueryTemplate< - TimelineQueryProps, - GetTimelineQuery.Query, - GetTimelineQuery.Variables -> { - private updatedDate: number = Date.now(); - private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; - - constructor(props: TimelineQueryProps) { - super(props); - this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); - } - - public render() { - const { - children, - clearSignalsState, - eventType = 'raw', - id, - indexPattern, - indexToAdd = [], - isInspected, - kibana, - limit, - fields, - filterQuery, - sourceId, - sortField, - } = this.props; - const defaultKibanaIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = - indexPattern == null || (indexPattern != null && indexPattern.title === '') - ? [ - ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), - ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), - ] - : indexPattern?.title.split(',') ?? []; - const variables: GetTimelineQuery.Variables = { - fieldRequested: fields, - filterQuery: createFilter(filterQuery), - sourceId, - pagination: { limit, cursor: null, tiebreaker: null }, - sortField, - defaultIndex, - inspect: isInspected, - }; - - return ( - <Query<GetTimelineQuery.Query, GetTimelineQuery.Variables> - query={timelineQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, fetchMore, refetch }) => { - this.setRefetch(refetch); - this.setExecuteBeforeRefetch(clearSignalsState); - this.setExecuteBeforeFetchMore(clearSignalsState); - - const timelineEdges = getOr([], 'source.Timeline.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ - variables: { - pagination: { - cursor: newCursor, - tiebreaker, - limit, - }, - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Timeline: { - ...fetchMoreResult.source.Timeline, - edges: uniqBy('node._id', [ - ...prev.source.Timeline.edges, - ...fetchMoreResult.source.Timeline.edges, - ]), - }, - }, - }; - }, - })); - this.updatedDate = Date.now(); - return children!({ - id, - inspect: getOr(null, 'source.Timeline.inspect', data), - refetch: this.wrappedRefetch, - loading, - totalCount: getOr(0, 'source.Timeline.totalCount', data), - pageInfo: getOr({}, 'source.Timeline.pageInfo', data), - events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), - loadMore: this.wrappedLoadMore, - getUpdatedAt: this.getUpdatedAt, - }); - }} - </Query> - ); - } - - private getUpdatedAt = () => this.updatedDate; - - private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => - timelineEdges.map((e: TimelineEdges) => e.node); -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.timelineQueryByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { - dispatch(timelineActions.clearEventsLoading({ id })); - dispatch(timelineActions.clearEventsDeleted({ id })); - } - }, -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const TimelineQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(TimelineQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx deleted file mode 100644 index 20617b88bda94..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - PageInfoPaginated, - TlsEdges, - TlsSortField, - GetTlsQuery, - FlowTargetSourceDest, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { tlsQuery } from './index.gql_query'; - -const ID = 'tlsQuery'; - -export interface TlsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - tls: TlsEdges[]; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: TlsArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip: string; - type: networkModel.NetworkType; -} - -export interface TlsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: TlsSortField; -} - -type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; - -class TlsComponentQuery extends QueryTemplatePaginated< - TlsProps, - GetTlsQuery.Query, - GetTlsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetTlsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate ? startDate : 0, - to: endDate ? endDate : Date.now(), - }, - }; - return ( - <Query<GetTlsQuery.Query, GetTlsQuery.Variables> - query={tlsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const tls = getOr([], 'source.Tls.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Tls: { - ...fetchMoreResult.source.Tls, - edges: [...fetchMoreResult.source.Tls.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Tls.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Tls.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - tls, - totalCount: getOr(-1, 'source.Tls.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTlsSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const TlsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(TlsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx deleted file mode 100644 index 72e4e46bc6ae0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetUncommonProcessesQuery, - PageInfoPaginated, - UncommonProcessesEdges, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { uncommonProcessesQuery } from './index.gql_query'; - -const ID = 'uncommonProcessesQuery'; - -export interface UncommonProcessesArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - uncommonProcesses: UncommonProcessesEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UncommonProcessesArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UncommonProcessesComponentQuery extends QueryTemplatePaginated< - UncommonProcessesProps, - GetUncommonProcessesQuery.Query, - GetUncommonProcessesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetUncommonProcessesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - pagination: generateTablePaginationOptions(activePage, limit), - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables> - query={uncommonProcessesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - UncommonProcesses: { - ...fetchMoreResult.source.UncommonProcesses, - edges: [...fetchMoreResult.source.UncommonProcesses.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.UncommonProcesses.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), - uncommonProcesses, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUncommonProcessesSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const UncommonProcessesQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(UncommonProcessesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx b/x-pack/legacy/plugins/siem/public/containers/users/index.tsx deleted file mode 100644 index 658cb5785b54c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { usersQuery } from './index.gql_query'; - -const ID = 'usersQuery'; - -export interface UsersArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - users: UsersEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UsersArgs) => React.ReactNode; - flowTarget: FlowTarget; - ip: string; - type: networkModel.NetworkType; -} - -type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UsersComponentQuery extends QueryTemplatePaginated< - UsersProps, - GetUsersQuery.Query, - GetUsersQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetUsersQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetUsersQuery.Query, GetUsersQuery.Variables> - query={usersQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const users = getOr([], `source.Users.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Users: { - ...fetchMoreResult.source.Users, - edges: [...fetchMoreResult.source.Users.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Users.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Users.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Users.totalCount', data), - users, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUsersSelector(state), - isInspected, - }; - }; - - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const UsersQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(UsersComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts deleted file mode 100644 index e15c099a007ad..0000000000000 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ /dev/null @@ -1,5943 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type Maybe<T> = T | null; - -export interface PageInfoNote { - pageIndex: number; - - pageSize: number; -} - -export interface SortNote { - sortField: SortFieldNote; - - sortOrder: Direction; -} - -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: number; - /** The beginning of the timerange */ - from: number; -} - -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export interface PaginationInput { - /** The limit parameter allows you to configure the maximum amount of items to be returned */ - limit: number; - /** The cursor parameter defines the next result you want to fetch */ - cursor?: Maybe<string>; - /** The tiebreaker parameter allow to be more precise to fetch the next item */ - tiebreaker?: Maybe<string>; -} - -export interface SortField { - sortFieldId: string; - - direction: Direction; -} - -export interface LastTimeDetails { - hostName?: Maybe<string>; - - ip?: Maybe<string>; -} - -export interface HostsSortField { - field: HostsFields; - - direction: Direction; -} - -export interface UsersSortField { - field: UsersFields; - - direction: Direction; -} - -export interface NetworkTopTablesSortField { - field: NetworkTopTablesFields; - - direction: Direction; -} - -export interface NetworkDnsSortField { - field: NetworkDnsFields; - - direction: Direction; -} - -export interface NetworkHttpSortField { - direction: Direction; -} - -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - -export interface PageInfoTimeline { - pageIndex: number; - - pageSize: number; -} - -export interface SortTimeline { - sortField: SortFieldTimeline; - - sortOrder: Direction; -} - -export interface NoteInput { - eventId?: Maybe<string>; - - note?: Maybe<string>; - - timelineId?: Maybe<string>; -} - -export interface TimelineInput { - columns?: Maybe<ColumnHeaderInput[]>; - - dataProviders?: Maybe<DataProviderInput[]>; - - description?: Maybe<string>; - - eventType?: Maybe<string>; - - filters?: Maybe<FilterTimelineInput[]>; - - kqlMode?: Maybe<string>; - - kqlQuery?: Maybe<SerializedFilterQueryInput>; - - title?: Maybe<string>; - - dateRange?: Maybe<DateRangePickerInput>; - - savedQueryId?: Maybe<string>; - - sort?: Maybe<SortTimelineInput>; -} - -export interface ColumnHeaderInput { - aggregatable?: Maybe<boolean>; - - category?: Maybe<string>; - - columnHeaderType?: Maybe<string>; - - description?: Maybe<string>; - - example?: Maybe<string>; - - indexes?: Maybe<string[]>; - - id?: Maybe<string>; - - name?: Maybe<string>; - - placeholder?: Maybe<string>; - - searchable?: Maybe<boolean>; - - type?: Maybe<string>; -} - -export interface DataProviderInput { - id?: Maybe<string>; - - name?: Maybe<string>; - - enabled?: Maybe<boolean>; - - excluded?: Maybe<boolean>; - - kqlQuery?: Maybe<string>; - - queryMatch?: Maybe<QueryMatchInput>; - - and?: Maybe<DataProviderInput[]>; -} - -export interface QueryMatchInput { - field?: Maybe<string>; - - displayField?: Maybe<string>; - - value?: Maybe<string>; - - displayValue?: Maybe<string>; - - operator?: Maybe<string>; -} - -export interface FilterTimelineInput { - exists?: Maybe<string>; - - meta?: Maybe<FilterMetaTimelineInput>; - - match_all?: Maybe<string>; - - missing?: Maybe<string>; - - query?: Maybe<string>; - - range?: Maybe<string>; - - script?: Maybe<string>; -} - -export interface FilterMetaTimelineInput { - alias?: Maybe<string>; - - controlledBy?: Maybe<string>; - - disabled?: Maybe<boolean>; - - field?: Maybe<string>; - - formattedValue?: Maybe<string>; - - index?: Maybe<string>; - - key?: Maybe<string>; - - negate?: Maybe<boolean>; - - params?: Maybe<string>; - - type?: Maybe<string>; - - value?: Maybe<string>; -} - -export interface SerializedFilterQueryInput { - filterQuery?: Maybe<SerializedKueryQueryInput>; -} - -export interface SerializedKueryQueryInput { - kuery?: Maybe<KueryFilterQueryInput>; - - serializedQuery?: Maybe<string>; -} - -export interface KueryFilterQueryInput { - kind?: Maybe<string>; - - expression?: Maybe<string>; -} - -export interface DateRangePickerInput { - start?: Maybe<number>; - - end?: Maybe<number>; -} - -export interface SortTimelineInput { - columnId?: Maybe<string>; - - sortDirection?: Maybe<string>; -} - -export interface FavoriteTimelineInput { - fullName?: Maybe<string>; - - userName?: Maybe<string>; - - favoriteDate?: Maybe<number>; -} - -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export enum LastEventIndexKey { - hostDetails = 'hostDetails', - hosts = 'hosts', - ipDetails = 'ipDetails', - network = 'network', -} - -export enum HostsFields { - hostName = 'hostName', - lastSeen = 'lastSeen', -} - -export enum UsersFields { - name = 'name', - count = 'count', -} - -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - -export enum HistogramType { - authentications = 'authentications', - anomalies = 'anomalies', - events = 'events', - alerts = 'alerts', - dns = 'dns', -} - -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - -export enum NetworkTopTablesFields { - bytes_in = 'bytes_in', - bytes_out = 'bytes_out', - flows = 'flows', - destination_ips = 'destination_ips', - source_ips = 'source_ips', -} - -export enum NetworkDnsFields { - dnsName = 'dnsName', - queryCount = 'queryCount', - uniqueDomains = 'uniqueDomains', - dnsBytesIn = 'dnsBytesIn', - dnsBytesOut = 'dnsBytesOut', -} - -export enum TlsFields { - _id = '_id', -} - -export enum SortFieldTimeline { - title = 'title', - description = 'description', - updated = 'updated', - created = 'created', -} - -export enum NetworkDirectionEcs { - inbound = 'inbound', - outbound = 'outbound', - internal = 'internal', - external = 'external', - incoming = 'incoming', - outgoing = 'outgoing', - listening = 'listening', - unknown = 'unknown', -} - -export enum NetworkHttpFields { - domains = 'domains', - lastHost = 'lastHost', - lastSourceIp = 'lastSourceIp', - methods = 'methods', - path = 'path', - requestCount = 'requestCount', - statuses = 'statuses', -} - -export enum FlowDirection { - uniDirectional = 'uniDirectional', - biDirectional = 'biDirectional', -} - -export type ToStringArray = string[]; - -export type Date = string; - -export type ToNumberArray = number[]; - -export type ToDateArray = string[]; - -export type ToBooleanArray = boolean[]; - -export type ToAny = any; - -export type EsValue = any; - -// ==================================================== -// Scalars -// ==================================================== - -// ==================================================== -// Types -// ==================================================== - -export interface Query { - getNote: NoteResult; - - getNotesByTimelineId: NoteResult[]; - - getNotesByEventId: NoteResult[]; - - getAllNotes: ResponseNotes; - - getAllPinnedEventsByTimelineId: PinnedEvent[]; - /** Get a security data source by id */ - source: Source; - /** Get a list of all security data sources */ - allSources: Source[]; - - getOneTimeline: TimelineResult; - - getAllTimeline: ResponseTimelines; -} - -export interface NoteResult { - eventId?: Maybe<string>; - - note?: Maybe<string>; - - timelineId?: Maybe<string>; - - noteId: string; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - timelineVersion?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version?: Maybe<string>; -} - -export interface ResponseNotes { - notes: NoteResult[]; - - totalCount?: Maybe<number>; -} - -export interface PinnedEvent { - code?: Maybe<number>; - - message?: Maybe<string>; - - pinnedEventId: string; - - eventId?: Maybe<string>; - - timelineId?: Maybe<string>; - - timelineVersion?: Maybe<string>; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version?: Maybe<string>; -} - -export interface Source { - /** The id of the source */ - id: string; - /** The raw configuration of the source */ - configuration: SourceConfiguration; - /** The status of the source */ - status: SourceStatus; - /** Gets Authentication success and failures based on a timerange */ - Authentications: AuthenticationsData; - - Timeline: TimelineData; - - TimelineDetails: TimelineDetailsData; - - LastEventTime: LastEventTimeData; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ - Hosts: HostsData; - - HostOverview: HostItem; - - HostFirstLastSeen: FirstLastSeenHost; - - IpOverview?: Maybe<IpOverviewData>; - - Users: UsersData; - - KpiNetwork?: Maybe<KpiNetworkData>; - - KpiHosts: KpiHostsData; - - KpiHostDetails: KpiHostDetailsData; - - MatrixHistogram: MatrixHistogramOverTimeData; - - NetworkTopCountries: NetworkTopCountriesData; - - NetworkTopNFlow: NetworkTopNFlowData; - - NetworkDns: NetworkDnsData; - - NetworkDnsHistogram: NetworkDsOverTimeData; - - NetworkHttp: NetworkHttpData; - - OverviewNetwork?: Maybe<OverviewNetworkData>; - - OverviewHost?: Maybe<OverviewHostData>; - - Tls: TlsData; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses: UncommonProcessesData; - /** Just a simple example to get the app name */ - whoAmI?: Maybe<SayMyName>; -} - -/** A set of configuration options for a security data source */ -export interface SourceConfiguration { - /** The field mapping to use for this source */ - fields: SourceFields; -} - -/** A mapping of semantic fields to their document counterparts */ -export interface SourceFields { - /** The field to identify a container by */ - container: string; - /** The fields to identify a host by */ - host: string; - /** The fields that may contain the log event message. The first field found win. */ - message: string[]; - /** The field to identify a pod by */ - pod: string; - /** The field to use as a tiebreaker for log events that have identical timestamps */ - tiebreaker: string; - /** The field to use as a timestamp for metrics and logs */ - timestamp: string; -} - -/** The status of an infrastructure data source */ -export interface SourceStatus { - /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ - indicesExist: boolean; - /** The list of fields defined in the index mappings */ - indexFields: IndexField[]; -} - -/** A descriptor of a field in an index */ -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe<string>; - /** whether the field's belong to an alias index */ - indexes: (Maybe<string>)[]; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe<string>; - - format?: Maybe<string>; -} - -export interface AuthenticationsData { - edges: AuthenticationsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface AuthenticationsEdges { - node: AuthenticationItem; - - cursor: CursorType; -} - -export interface AuthenticationItem { - _id: string; - - failures: number; - - successes: number; - - user: UserEcsFields; - - lastSuccess?: Maybe<LastSourceHost>; - - lastFailure?: Maybe<LastSourceHost>; -} - -export interface UserEcsFields { - domain?: Maybe<string[]>; - - id?: Maybe<string[]>; - - name?: Maybe<string[]>; - - full_name?: Maybe<string[]>; - - email?: Maybe<string[]>; - - hash?: Maybe<string[]>; - - group?: Maybe<string[]>; -} - -export interface LastSourceHost { - timestamp?: Maybe<string>; - - source?: Maybe<SourceEcsFields>; - - host?: Maybe<HostEcsFields>; -} - -export interface SourceEcsFields { - bytes?: Maybe<number[]>; - - ip?: Maybe<string[]>; - - port?: Maybe<number[]>; - - domain?: Maybe<string[]>; - - geo?: Maybe<GeoEcsFields>; - - packets?: Maybe<number[]>; -} - -export interface GeoEcsFields { - city_name?: Maybe<string[]>; - - continent_name?: Maybe<string[]>; - - country_iso_code?: Maybe<string[]>; - - country_name?: Maybe<string[]>; - - location?: Maybe<Location>; - - region_iso_code?: Maybe<string[]>; - - region_name?: Maybe<string[]>; -} - -export interface Location { - lon?: Maybe<number[]>; - - lat?: Maybe<number[]>; -} - -export interface HostEcsFields { - architecture?: Maybe<string[]>; - - id?: Maybe<string[]>; - - ip?: Maybe<string[]>; - - mac?: Maybe<string[]>; - - name?: Maybe<string[]>; - - os?: Maybe<OsEcsFields>; - - type?: Maybe<string[]>; -} - -export interface OsEcsFields { - platform?: Maybe<string[]>; - - name?: Maybe<string[]>; - - full?: Maybe<string[]>; - - family?: Maybe<string[]>; - - version?: Maybe<string[]>; - - kernel?: Maybe<string[]>; -} - -export interface CursorType { - value?: Maybe<string>; - - tiebreaker?: Maybe<string>; -} - -export interface PageInfoPaginated { - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface TimelineData { - edges: TimelineEdges[]; - - totalCount: number; - - pageInfo: PageInfo; - - inspect?: Maybe<Inspect>; -} - -export interface TimelineEdges { - node: TimelineItem; - - cursor: CursorType; -} - -export interface TimelineItem { - _id: string; - - _index?: Maybe<string>; - - data: TimelineNonEcsData[]; - - ecs: Ecs; -} - -export interface TimelineNonEcsData { - field: string; - - value?: Maybe<string[]>; -} - -export interface Ecs { - _id: string; - - _index?: Maybe<string>; - - auditd?: Maybe<AuditdEcsFields>; - - destination?: Maybe<DestinationEcsFields>; - - dns?: Maybe<DnsEcsFields>; - - endgame?: Maybe<EndgameEcsFields>; - - event?: Maybe<EventEcsFields>; - - geo?: Maybe<GeoEcsFields>; - - host?: Maybe<HostEcsFields>; - - network?: Maybe<NetworkEcsField>; - - rule?: Maybe<RuleEcsField>; - - signal?: Maybe<SignalField>; - - source?: Maybe<SourceEcsFields>; - - suricata?: Maybe<SuricataEcsFields>; - - tls?: Maybe<TlsEcsFields>; - - zeek?: Maybe<ZeekEcsFields>; - - http?: Maybe<HttpEcsFields>; - - url?: Maybe<UrlEcsFields>; - - timestamp?: Maybe<string>; - - message?: Maybe<string[]>; - - user?: Maybe<UserEcsFields>; - - winlog?: Maybe<WinlogEcsFields>; - - process?: Maybe<ProcessEcsFields>; - - file?: Maybe<FileFields>; - - system?: Maybe<SystemEcsField>; -} - -export interface AuditdEcsFields { - result?: Maybe<string[]>; - - session?: Maybe<string[]>; - - data?: Maybe<AuditdData>; - - summary?: Maybe<Summary>; - - sequence?: Maybe<string[]>; -} - -export interface AuditdData { - acct?: Maybe<string[]>; - - terminal?: Maybe<string[]>; - - op?: Maybe<string[]>; -} - -export interface Summary { - actor?: Maybe<PrimarySecondary>; - - object?: Maybe<PrimarySecondary>; - - how?: Maybe<string[]>; - - message_type?: Maybe<string[]>; - - sequence?: Maybe<string[]>; -} - -export interface PrimarySecondary { - primary?: Maybe<string[]>; - - secondary?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface DestinationEcsFields { - bytes?: Maybe<number[]>; - - ip?: Maybe<string[]>; - - port?: Maybe<number[]>; - - domain?: Maybe<string[]>; - - geo?: Maybe<GeoEcsFields>; - - packets?: Maybe<number[]>; -} - -export interface DnsEcsFields { - question?: Maybe<DnsQuestionData>; - - resolved_ip?: Maybe<string[]>; - - response_code?: Maybe<string[]>; -} - -export interface DnsQuestionData { - name?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface EndgameEcsFields { - exit_code?: Maybe<number[]>; - - file_name?: Maybe<string[]>; - - file_path?: Maybe<string[]>; - - logon_type?: Maybe<number[]>; - - parent_process_name?: Maybe<string[]>; - - pid?: Maybe<number[]>; - - process_name?: Maybe<string[]>; - - subject_domain_name?: Maybe<string[]>; - - subject_logon_id?: Maybe<string[]>; - - subject_user_name?: Maybe<string[]>; - - target_domain_name?: Maybe<string[]>; - - target_logon_id?: Maybe<string[]>; - - target_user_name?: Maybe<string[]>; -} - -export interface EventEcsFields { - action?: Maybe<string[]>; - - category?: Maybe<string[]>; - - code?: Maybe<string[]>; - - created?: Maybe<string[]>; - - dataset?: Maybe<string[]>; - - duration?: Maybe<number[]>; - - end?: Maybe<string[]>; - - hash?: Maybe<string[]>; - - id?: Maybe<string[]>; - - kind?: Maybe<string[]>; - - module?: Maybe<string[]>; - - original?: Maybe<string[]>; - - outcome?: Maybe<string[]>; - - risk_score?: Maybe<number[]>; - - risk_score_norm?: Maybe<number[]>; - - severity?: Maybe<number[]>; - - start?: Maybe<string[]>; - - timezone?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface NetworkEcsField { - bytes?: Maybe<number[]>; - - community_id?: Maybe<string[]>; - - direction?: Maybe<string[]>; - - packets?: Maybe<number[]>; - - protocol?: Maybe<string[]>; - - transport?: Maybe<string[]>; -} - -export interface RuleEcsField { - reference?: Maybe<string[]>; -} - -export interface SignalField { - rule?: Maybe<RuleField>; - - original_time?: Maybe<string[]>; -} - -export interface RuleField { - id?: Maybe<string[]>; - - rule_id?: Maybe<string[]>; - - false_positives: string[]; - - saved_id?: Maybe<string[]>; - - timeline_id?: Maybe<string[]>; - - timeline_title?: Maybe<string[]>; - - max_signals?: Maybe<number[]>; - - risk_score?: Maybe<string[]>; - - output_index?: Maybe<string[]>; - - description?: Maybe<string[]>; - - from?: Maybe<string[]>; - - immutable?: Maybe<boolean[]>; - - index?: Maybe<string[]>; - - interval?: Maybe<string[]>; - - language?: Maybe<string[]>; - - query?: Maybe<string[]>; - - references?: Maybe<string[]>; - - severity?: Maybe<string[]>; - - tags?: Maybe<string[]>; - - threat?: Maybe<ToAny>; - - type?: Maybe<string[]>; - - size?: Maybe<string[]>; - - to?: Maybe<string[]>; - - enabled?: Maybe<boolean[]>; - - filters?: Maybe<ToAny>; - - created_at?: Maybe<string[]>; - - updated_at?: Maybe<string[]>; - - created_by?: Maybe<string[]>; - - updated_by?: Maybe<string[]>; - - version?: Maybe<string[]>; - - note?: Maybe<string[]>; -} - -export interface SuricataEcsFields { - eve?: Maybe<SuricataEveData>; -} - -export interface SuricataEveData { - alert?: Maybe<SuricataAlertData>; - - flow_id?: Maybe<number[]>; - - proto?: Maybe<string[]>; -} - -export interface SuricataAlertData { - signature?: Maybe<string[]>; - - signature_id?: Maybe<number[]>; -} - -export interface TlsEcsFields { - client_certificate?: Maybe<TlsClientCertificateData>; - - fingerprints?: Maybe<TlsFingerprintsData>; - - server_certificate?: Maybe<TlsServerCertificateData>; -} - -export interface TlsClientCertificateData { - fingerprint?: Maybe<FingerprintData>; -} - -export interface FingerprintData { - sha1?: Maybe<string[]>; -} - -export interface TlsFingerprintsData { - ja3?: Maybe<TlsJa3Data>; -} - -export interface TlsJa3Data { - hash?: Maybe<string[]>; -} - -export interface TlsServerCertificateData { - fingerprint?: Maybe<FingerprintData>; -} - -export interface ZeekEcsFields { - session_id?: Maybe<string[]>; - - connection?: Maybe<ZeekConnectionData>; - - notice?: Maybe<ZeekNoticeData>; - - dns?: Maybe<ZeekDnsData>; - - http?: Maybe<ZeekHttpData>; - - files?: Maybe<ZeekFileData>; - - ssl?: Maybe<ZeekSslData>; -} - -export interface ZeekConnectionData { - local_resp?: Maybe<boolean[]>; - - local_orig?: Maybe<boolean[]>; - - missed_bytes?: Maybe<number[]>; - - state?: Maybe<string[]>; - - history?: Maybe<string[]>; -} - -export interface ZeekNoticeData { - suppress_for?: Maybe<number[]>; - - msg?: Maybe<string[]>; - - note?: Maybe<string[]>; - - sub?: Maybe<string[]>; - - dst?: Maybe<string[]>; - - dropped?: Maybe<boolean[]>; - - peer_descr?: Maybe<string[]>; -} - -export interface ZeekDnsData { - AA?: Maybe<boolean[]>; - - qclass_name?: Maybe<string[]>; - - RD?: Maybe<boolean[]>; - - qtype_name?: Maybe<string[]>; - - rejected?: Maybe<boolean[]>; - - qtype?: Maybe<string[]>; - - query?: Maybe<string[]>; - - trans_id?: Maybe<number[]>; - - qclass?: Maybe<string[]>; - - RA?: Maybe<boolean[]>; - - TC?: Maybe<boolean[]>; -} - -export interface ZeekHttpData { - resp_mime_types?: Maybe<string[]>; - - trans_depth?: Maybe<string[]>; - - status_msg?: Maybe<string[]>; - - resp_fuids?: Maybe<string[]>; - - tags?: Maybe<string[]>; -} - -export interface ZeekFileData { - session_ids?: Maybe<string[]>; - - timedout?: Maybe<boolean[]>; - - local_orig?: Maybe<boolean[]>; - - tx_host?: Maybe<string[]>; - - source?: Maybe<string[]>; - - is_orig?: Maybe<boolean[]>; - - overflow_bytes?: Maybe<number[]>; - - sha1?: Maybe<string[]>; - - duration?: Maybe<number[]>; - - depth?: Maybe<number[]>; - - analyzers?: Maybe<string[]>; - - mime_type?: Maybe<string[]>; - - rx_host?: Maybe<string[]>; - - total_bytes?: Maybe<number[]>; - - fuid?: Maybe<string[]>; - - seen_bytes?: Maybe<number[]>; - - missing_bytes?: Maybe<number[]>; - - md5?: Maybe<string[]>; -} - -export interface ZeekSslData { - cipher?: Maybe<string[]>; - - established?: Maybe<boolean[]>; - - resumed?: Maybe<boolean[]>; - - version?: Maybe<string[]>; -} - -export interface HttpEcsFields { - version?: Maybe<string[]>; - - request?: Maybe<HttpRequestData>; - - response?: Maybe<HttpResponseData>; -} - -export interface HttpRequestData { - method?: Maybe<string[]>; - - body?: Maybe<HttpBodyData>; - - referrer?: Maybe<string[]>; - - bytes?: Maybe<number[]>; -} - -export interface HttpBodyData { - content?: Maybe<string[]>; - - bytes?: Maybe<number[]>; -} - -export interface HttpResponseData { - status_code?: Maybe<number[]>; - - body?: Maybe<HttpBodyData>; - - bytes?: Maybe<number[]>; -} - -export interface UrlEcsFields { - domain?: Maybe<string[]>; - - original?: Maybe<string[]>; - - username?: Maybe<string[]>; - - password?: Maybe<string[]>; -} - -export interface WinlogEcsFields { - event_id?: Maybe<number[]>; -} - -export interface ProcessEcsFields { - hash?: Maybe<ProcessHashData>; - - pid?: Maybe<number[]>; - - name?: Maybe<string[]>; - - ppid?: Maybe<number[]>; - - args?: Maybe<string[]>; - - executable?: Maybe<string[]>; - - title?: Maybe<string[]>; - - thread?: Maybe<Thread>; - - working_directory?: Maybe<string[]>; -} - -export interface ProcessHashData { - md5?: Maybe<string[]>; - - sha1?: Maybe<string[]>; - - sha256?: Maybe<string[]>; -} - -export interface Thread { - id?: Maybe<number[]>; - - start?: Maybe<string[]>; -} - -export interface FileFields { - name?: Maybe<string[]>; - - path?: Maybe<string[]>; - - target_path?: Maybe<string[]>; - - extension?: Maybe<string[]>; - - type?: Maybe<string[]>; - - device?: Maybe<string[]>; - - inode?: Maybe<string[]>; - - uid?: Maybe<string[]>; - - owner?: Maybe<string[]>; - - gid?: Maybe<string[]>; - - group?: Maybe<string[]>; - - mode?: Maybe<string[]>; - - size?: Maybe<number[]>; - - mtime?: Maybe<string[]>; - - ctime?: Maybe<string[]>; -} - -export interface SystemEcsField { - audit?: Maybe<AuditEcsFields>; - - auth?: Maybe<AuthEcsFields>; -} - -export interface AuditEcsFields { - package?: Maybe<PackageEcsFields>; -} - -export interface PackageEcsFields { - arch?: Maybe<string[]>; - - entity_id?: Maybe<string[]>; - - name?: Maybe<string[]>; - - size?: Maybe<number[]>; - - summary?: Maybe<string[]>; - - version?: Maybe<string[]>; -} - -export interface AuthEcsFields { - ssh?: Maybe<SshEcsFields>; -} - -export interface SshEcsFields { - method?: Maybe<string[]>; - - signature?: Maybe<string[]>; -} - -export interface PageInfo { - endCursor?: Maybe<CursorType>; - - hasNextPage?: Maybe<boolean>; -} - -export interface TimelineDetailsData { - data?: Maybe<DetailItem[]>; - - inspect?: Maybe<Inspect>; -} - -export interface DetailItem { - field: string; - - values?: Maybe<string[]>; - - originalValue?: Maybe<EsValue>; -} - -export interface LastEventTimeData { - lastSeen?: Maybe<string>; - - inspect?: Maybe<Inspect>; -} - -export interface HostsData { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostItem { - _id?: Maybe<string>; - - lastSeen?: Maybe<string>; - - host?: Maybe<HostEcsFields>; - - cloud?: Maybe<CloudFields>; - - inspect?: Maybe<Inspect>; -} - -export interface CloudFields { - instance?: Maybe<CloudInstance>; - - machine?: Maybe<CloudMachine>; - - provider?: Maybe<(Maybe<string>)[]>; - - region?: Maybe<(Maybe<string>)[]>; -} - -export interface CloudInstance { - id?: Maybe<(Maybe<string>)[]>; -} - -export interface CloudMachine { - type?: Maybe<(Maybe<string>)[]>; -} - -export interface FirstLastSeenHost { - inspect?: Maybe<Inspect>; - - firstSeen?: Maybe<string>; - - lastSeen?: Maybe<string>; -} - -export interface IpOverviewData { - client?: Maybe<Overview>; - - destination?: Maybe<Overview>; - - host: HostEcsFields; - - server?: Maybe<Overview>; - - source?: Maybe<Overview>; - - inspect?: Maybe<Inspect>; -} - -export interface Overview { - firstSeen?: Maybe<string>; - - lastSeen?: Maybe<string>; - - autonomousSystem: AutonomousSystem; - - geo: GeoEcsFields; -} - -export interface AutonomousSystem { - number?: Maybe<number>; - - organization?: Maybe<AutonomousSystemOrganization>; -} - -export interface AutonomousSystemOrganization { - name?: Maybe<string>; -} - -export interface UsersData { - edges: UsersEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface UsersEdges { - node: UsersNode; - - cursor: CursorType; -} - -export interface UsersNode { - _id?: Maybe<string>; - - timestamp?: Maybe<string>; - - user?: Maybe<UsersItem>; -} - -export interface UsersItem { - name?: Maybe<string>; - - id?: Maybe<string[]>; - - groupId?: Maybe<string[]>; - - groupName?: Maybe<string[]>; - - count?: Maybe<number>; -} - -export interface KpiNetworkData { - networkEvents?: Maybe<number>; - - uniqueFlowId?: Maybe<number>; - - uniqueSourcePrivateIps?: Maybe<number>; - - uniqueSourcePrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; - - uniqueDestinationPrivateIps?: Maybe<number>; - - uniqueDestinationPrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; - - dnsQueries?: Maybe<number>; - - tlsHandshakes?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface KpiNetworkHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; -} - -export interface KpiHostsData { - hosts?: Maybe<number>; - - hostsHistogram?: Maybe<KpiHostHistogramData[]>; - - authSuccess?: Maybe<number>; - - authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; - - authFailure?: Maybe<number>; - - authFailureHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueSourceIps?: Maybe<number>; - - uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueDestinationIps?: Maybe<number>; - - uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - inspect?: Maybe<Inspect>; -} - -export interface KpiHostHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; -} - -export interface KpiHostDetailsData { - authSuccess?: Maybe<number>; - - authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; - - authFailure?: Maybe<number>; - - authFailureHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueSourceIps?: Maybe<number>; - - uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueDestinationIps?: Maybe<number>; - - uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - inspect?: Maybe<Inspect>; -} - -export interface MatrixHistogramOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface MatrixOverTimeHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; - - g?: Maybe<string>; -} - -export interface NetworkTopCountriesData { - edges: NetworkTopCountriesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkTopCountriesEdges { - node: NetworkTopCountriesItem; - - cursor: CursorType; -} - -export interface NetworkTopCountriesItem { - _id?: Maybe<string>; - - source?: Maybe<TopCountriesItemSource>; - - destination?: Maybe<TopCountriesItemDestination>; - - network?: Maybe<TopNetworkTablesEcsField>; -} - -export interface TopCountriesItemSource { - country?: Maybe<string>; - - destination_ips?: Maybe<number>; - - flows?: Maybe<number>; - - location?: Maybe<GeoItem>; - - source_ips?: Maybe<number>; -} - -export interface GeoItem { - geo?: Maybe<GeoEcsFields>; - - flowTarget?: Maybe<FlowTargetSourceDest>; -} - -export interface TopCountriesItemDestination { - country?: Maybe<string>; - - destination_ips?: Maybe<number>; - - flows?: Maybe<number>; - - location?: Maybe<GeoItem>; - - source_ips?: Maybe<number>; -} - -export interface TopNetworkTablesEcsField { - bytes_in?: Maybe<number>; - - bytes_out?: Maybe<number>; -} - -export interface NetworkTopNFlowData { - edges: NetworkTopNFlowEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkTopNFlowEdges { - node: NetworkTopNFlowItem; - - cursor: CursorType; -} - -export interface NetworkTopNFlowItem { - _id?: Maybe<string>; - - source?: Maybe<TopNFlowItemSource>; - - destination?: Maybe<TopNFlowItemDestination>; - - network?: Maybe<TopNetworkTablesEcsField>; -} - -export interface TopNFlowItemSource { - autonomous_system?: Maybe<AutonomousSystemItem>; - - domain?: Maybe<string[]>; - - ip?: Maybe<string>; - - location?: Maybe<GeoItem>; - - flows?: Maybe<number>; - - destination_ips?: Maybe<number>; -} - -export interface AutonomousSystemItem { - name?: Maybe<string>; - - number?: Maybe<number>; -} - -export interface TopNFlowItemDestination { - autonomous_system?: Maybe<AutonomousSystemItem>; - - domain?: Maybe<string[]>; - - ip?: Maybe<string>; - - location?: Maybe<GeoItem>; - - flows?: Maybe<number>; - - source_ips?: Maybe<number>; -} - -export interface NetworkDnsData { - edges: NetworkDnsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; - - histogram?: Maybe<MatrixOverOrdinalHistogramData[]>; -} - -export interface NetworkDnsEdges { - node: NetworkDnsItem; - - cursor: CursorType; -} - -export interface NetworkDnsItem { - _id?: Maybe<string>; - - dnsBytesIn?: Maybe<number>; - - dnsBytesOut?: Maybe<number>; - - dnsName?: Maybe<string>; - - queryCount?: Maybe<number>; - - uniqueDomains?: Maybe<number>; -} - -export interface MatrixOverOrdinalHistogramData { - x: string; - - y: number; - - g: string; -} - -export interface NetworkDsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface NetworkHttpData { - edges: NetworkHttpEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkHttpEdges { - node: NetworkHttpItem; - - cursor: CursorType; -} - -export interface NetworkHttpItem { - _id?: Maybe<string>; - - domains: string[]; - - lastHost?: Maybe<string>; - - lastSourceIp?: Maybe<string>; - - methods: string[]; - - path?: Maybe<string>; - - requestCount?: Maybe<number>; - - statuses: string[]; -} - -export interface OverviewNetworkData { - auditbeatSocket?: Maybe<number>; - - filebeatCisco?: Maybe<number>; - - filebeatNetflow?: Maybe<number>; - - filebeatPanw?: Maybe<number>; - - filebeatSuricata?: Maybe<number>; - - filebeatZeek?: Maybe<number>; - - packetbeatDNS?: Maybe<number>; - - packetbeatFlow?: Maybe<number>; - - packetbeatTLS?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface OverviewHostData { - auditbeatAuditd?: Maybe<number>; - - auditbeatFIM?: Maybe<number>; - - auditbeatLogin?: Maybe<number>; - - auditbeatPackage?: Maybe<number>; - - auditbeatProcess?: Maybe<number>; - - auditbeatUser?: Maybe<number>; - - endgameDns?: Maybe<number>; - - endgameFile?: Maybe<number>; - - endgameImageLoad?: Maybe<number>; - - endgameNetwork?: Maybe<number>; - - endgameProcess?: Maybe<number>; - - endgameRegistry?: Maybe<number>; - - endgameSecurity?: Maybe<number>; - - filebeatSystemModule?: Maybe<number>; - - winlogbeatSecurity?: Maybe<number>; - - winlogbeatMWSysmonOperational?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe<string>; - - timestamp?: Maybe<string>; - - notAfter?: Maybe<string[]>; - - subjects?: Maybe<string[]>; - - ja3?: Maybe<string[]>; - - issuers?: Maybe<string[]>; -} - -export interface UncommonProcessesData { - edges: UncommonProcessesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface UncommonProcessesEdges { - node: UncommonProcessItem; - - cursor: CursorType; -} - -export interface UncommonProcessItem { - _id: string; - - instances: number; - - process: ProcessEcsFields; - - hosts: HostEcsFields[]; - - user?: Maybe<UserEcsFields>; -} - -export interface SayMyName { - /** The id of the source */ - appName: string; -} - -export interface TimelineResult { - columns?: Maybe<ColumnHeaderResult[]>; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - dataProviders?: Maybe<DataProviderResult[]>; - - dateRange?: Maybe<DateRangePickerResult>; - - description?: Maybe<string>; - - eventIdToNoteIds?: Maybe<NoteResult[]>; - - eventType?: Maybe<string>; - - favorite?: Maybe<FavoriteTimelineResult[]>; - - filters?: Maybe<FilterTimelineResult[]>; - - kqlMode?: Maybe<string>; - - kqlQuery?: Maybe<SerializedFilterQueryResult>; - - notes?: Maybe<NoteResult[]>; - - noteIds?: Maybe<string[]>; - - pinnedEventIds?: Maybe<string[]>; - - pinnedEventsSaveObject?: Maybe<PinnedEvent[]>; - - savedQueryId?: Maybe<string>; - - savedObjectId: string; - - sort?: Maybe<SortTimelineResult>; - - title?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version: string; -} - -export interface ColumnHeaderResult { - aggregatable?: Maybe<boolean>; - - category?: Maybe<string>; - - columnHeaderType?: Maybe<string>; - - description?: Maybe<string>; - - example?: Maybe<string>; - - indexes?: Maybe<string[]>; - - id?: Maybe<string>; - - name?: Maybe<string>; - - placeholder?: Maybe<string>; - - searchable?: Maybe<boolean>; - - type?: Maybe<string>; -} - -export interface DataProviderResult { - id?: Maybe<string>; - - name?: Maybe<string>; - - enabled?: Maybe<boolean>; - - excluded?: Maybe<boolean>; - - kqlQuery?: Maybe<string>; - - queryMatch?: Maybe<QueryMatchResult>; - - and?: Maybe<DataProviderResult[]>; -} - -export interface QueryMatchResult { - field?: Maybe<string>; - - displayField?: Maybe<string>; - - value?: Maybe<string>; - - displayValue?: Maybe<string>; - - operator?: Maybe<string>; -} - -export interface DateRangePickerResult { - start?: Maybe<number>; - - end?: Maybe<number>; -} - -export interface FavoriteTimelineResult { - fullName?: Maybe<string>; - - userName?: Maybe<string>; - - favoriteDate?: Maybe<number>; -} - -export interface FilterTimelineResult { - exists?: Maybe<string>; - - meta?: Maybe<FilterMetaTimelineResult>; - - match_all?: Maybe<string>; - - missing?: Maybe<string>; - - query?: Maybe<string>; - - range?: Maybe<string>; - - script?: Maybe<string>; -} - -export interface FilterMetaTimelineResult { - alias?: Maybe<string>; - - controlledBy?: Maybe<string>; - - disabled?: Maybe<boolean>; - - field?: Maybe<string>; - - formattedValue?: Maybe<string>; - - index?: Maybe<string>; - - key?: Maybe<string>; - - negate?: Maybe<boolean>; - - params?: Maybe<string>; - - type?: Maybe<string>; - - value?: Maybe<string>; -} - -export interface SerializedFilterQueryResult { - filterQuery?: Maybe<SerializedKueryQueryResult>; -} - -export interface SerializedKueryQueryResult { - kuery?: Maybe<KueryFilterQueryResult>; - - serializedQuery?: Maybe<string>; -} - -export interface KueryFilterQueryResult { - kind?: Maybe<string>; - - expression?: Maybe<string>; -} - -export interface SortTimelineResult { - columnId?: Maybe<string>; - - sortDirection?: Maybe<string>; -} - -export interface ResponseTimelines { - timeline: (Maybe<TimelineResult>)[]; - - totalCount?: Maybe<number>; -} - -export interface Mutation { - /** Persists a note */ - persistNote: ResponseNote; - - deleteNote?: Maybe<boolean>; - - deleteNoteByTimelineId?: Maybe<boolean>; - /** Persists a pinned event in a timeline */ - persistPinnedEventOnTimeline?: Maybe<PinnedEvent>; - /** Remove a pinned events in a timeline */ - deletePinnedEventOnTimeline: boolean; - /** Remove all pinned events in a timeline */ - deleteAllPinnedEventsOnTimeline: boolean; - /** Persists a timeline */ - persistTimeline: ResponseTimeline; - - persistFavorite: ResponseFavoriteTimeline; - - deleteTimeline: boolean; -} - -export interface ResponseNote { - code?: Maybe<number>; - - message?: Maybe<string>; - - note: NoteResult; -} - -export interface ResponseTimeline { - code?: Maybe<number>; - - message?: Maybe<string>; - - timeline: TimelineResult; -} - -export interface ResponseFavoriteTimeline { - code?: Maybe<number>; - - message?: Maybe<string>; - - savedObjectId: string; - - version: string; - - favorite?: Maybe<FavoriteTimelineResult[]>; -} - -export interface EcsEdges { - node: Ecs; - - cursor: CursorType; -} - -export interface EventsTimelineData { - edges: EcsEdges[]; - - totalCount: number; - - pageInfo: PageInfo; - - inspect?: Maybe<Inspect>; -} - -export interface OsFields { - platform?: Maybe<string>; - - name?: Maybe<string>; - - full?: Maybe<string>; - - family?: Maybe<string>; - - version?: Maybe<string>; - - kernel?: Maybe<string>; -} - -export interface HostFields { - architecture?: Maybe<string>; - - id?: Maybe<string>; - - ip?: Maybe<(Maybe<string>)[]>; - - mac?: Maybe<(Maybe<string>)[]>; - - name?: Maybe<string>; - - os?: Maybe<OsFields>; - - type?: Maybe<string>; -} - -// ==================================================== -// Arguments -// ==================================================== - -export interface GetNoteQueryArgs { - id: string; -} -export interface GetNotesByTimelineIdQueryArgs { - timelineId: string; -} -export interface GetNotesByEventIdQueryArgs { - eventId: string; -} -export interface GetAllNotesQueryArgs { - pageInfo?: Maybe<PageInfoNote>; - - search?: Maybe<string>; - - sort?: Maybe<SortNote>; -} -export interface GetAllPinnedEventsByTimelineIdQueryArgs { - timelineId: string; -} -export interface SourceQueryArgs { - /** The id of the source */ - id: string; -} -export interface GetOneTimelineQueryArgs { - id: string; -} -export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe<PageInfoTimeline>; - - search?: Maybe<string>; - - sort?: Maybe<SortTimeline>; - - onlyUserFavorite?: Maybe<boolean>; -} -export interface AuthenticationsSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TimelineSourceArgs { - pagination: PaginationInput; - - sortField: SortField; - - fieldRequested: string[]; - - timerange?: Maybe<TimerangeInput>; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TimelineDetailsSourceArgs { - eventId: string; - - indexName: string; - - defaultIndex: string[]; -} -export interface LastEventTimeSourceArgs { - id?: Maybe<string>; - - indexKey: LastEventIndexKey; - - details: LastTimeDetails; - - defaultIndex: string[]; -} -export interface HostsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - sort: HostsSortField; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface HostOverviewSourceArgs { - id?: Maybe<string>; - - hostName: string; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface HostFirstLastSeenSourceArgs { - id?: Maybe<string>; - - hostName: string; - - defaultIndex: string[]; -} -export interface IpOverviewSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip: string; - - defaultIndex: string[]; -} -export interface UsersSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: UsersSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface KpiNetworkSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface KpiHostsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface KpiHostDetailsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface MatrixHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField: string; - - histogramType: HistogramType; -} -export interface NetworkTopCountriesSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - flowTarget: FlowTargetSourceDest; - - pagination: PaginationInputPaginated; - - sort: NetworkTopTablesSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkTopNFlowSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - flowTarget: FlowTargetSourceDest; - - pagination: PaginationInputPaginated; - - sort: NetworkTopTablesSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkDnsSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - isPtrIncluded: boolean; - - pagination: PaginationInputPaginated; - - sort: NetworkDnsSortField; - - stackByField?: Maybe<string>; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkDnsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface NetworkHttpSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - pagination: PaginationInputPaginated; - - sort: NetworkHttpSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface OverviewNetworkSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface OverviewHostSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TlsSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface UncommonProcessesSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface IndicesExistSourceStatusArgs { - defaultIndex: string[]; -} -export interface IndexFieldsSourceStatusArgs { - defaultIndex: string[]; -} -export interface PersistNoteMutationArgs { - noteId?: Maybe<string>; - - version?: Maybe<string>; - - note: NoteInput; -} -export interface DeleteNoteMutationArgs { - id: string[]; -} -export interface DeleteNoteByTimelineIdMutationArgs { - timelineId: string; - - version?: Maybe<string>; -} -export interface PersistPinnedEventOnTimelineMutationArgs { - pinnedEventId?: Maybe<string>; - - eventId: string; - - timelineId?: Maybe<string>; -} -export interface DeletePinnedEventOnTimelineMutationArgs { - id: string[]; -} -export interface DeleteAllPinnedEventsOnTimelineMutationArgs { - timelineId: string; -} -export interface PersistTimelineMutationArgs { - id?: Maybe<string>; - - version?: Maybe<string>; - - timeline: TimelineInput; -} -export interface PersistFavoriteMutationArgs { - timelineId?: Maybe<string>; -} -export interface DeleteTimelineMutationArgs { - id: string[]; -} - -// ==================================================== -// Documents -// ==================================================== - -export namespace GetAuthenticationsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Authentications: Authentications; - }; - - export type Authentications = { - __typename?: 'AuthenticationsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'AuthenticationsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'AuthenticationItem'; - - _id: string; - - failures: number; - - successes: number; - - user: User; - - lastSuccess: Maybe<LastSuccess>; - - lastFailure: Maybe<LastFailure>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - name: Maybe<string[]>; - }; - - export type LastSuccess = { - __typename?: 'LastSourceHost'; - - timestamp: Maybe<string>; - - source: Maybe<_Source>; - - host: Maybe<Host>; - }; - - export type _Source = { - __typename?: 'SourceEcsFields'; - - ip: Maybe<string[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type LastFailure = { - __typename?: 'LastSourceHost'; - - timestamp: Maybe<string>; - - source: Maybe<__Source>; - - host: Maybe<_Host>; - }; - - export type __Source = { - __typename?: 'SourceEcsFields'; - - ip: Maybe<string[]>; - }; - - export type _Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetLastEventTimeQuery { - export type Variables = { - sourceId: string; - indexKey: LastEventIndexKey; - details: LastTimeDetails; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - LastEventTime: LastEventTime; - }; - - export type LastEventTime = { - __typename?: 'LastEventTimeData'; - - lastSeen: Maybe<string>; - }; -} - -export namespace GetHostFirstLastSeenQuery { - export type Variables = { - sourceId: string; - hostName: string; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostFirstLastSeen: HostFirstLastSeen; - }; - - export type HostFirstLastSeen = { - __typename?: 'FirstLastSeenHost'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - }; -} - -export namespace GetHostsTableQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - sort: HostsSortField; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Hosts: Hosts; - }; - - export type Hosts = { - __typename?: 'HostsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'HostsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'HostItem'; - - _id: Maybe<string>; - - lastSeen: Maybe<string>; - - host: Maybe<Host>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - name: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetHostOverviewQuery { - export type Variables = { - sourceId: string; - hostName: string; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostOverview: HostOverview; - }; - - export type HostOverview = { - __typename?: 'HostItem'; - - _id: Maybe<string>; - - host: Maybe<Host>; - - cloud: Maybe<Cloud>; - - inspect: Maybe<Inspect>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe<string[]>; - - id: Maybe<string[]>; - - ip: Maybe<string[]>; - - mac: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - - type: Maybe<string[]>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe<string[]>; - - name: Maybe<string[]>; - - platform: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Cloud = { - __typename?: 'CloudFields'; - - instance: Maybe<Instance>; - - machine: Maybe<Machine>; - - provider: Maybe<(Maybe<string>)[]>; - - region: Maybe<(Maybe<string>)[]>; - }; - - export type Instance = { - __typename?: 'CloudInstance'; - - id: Maybe<(Maybe<string>)[]>; - }; - - export type Machine = { - __typename?: 'CloudMachine'; - - type: Maybe<(Maybe<string>)[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetIpOverviewQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - ip: string; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - IpOverview: Maybe<IpOverview>; - }; - - export type IpOverview = { - __typename?: 'IpOverviewData'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - host: Host; - - inspect: Maybe<Inspect>; - }; - - export type _Source = { - __typename?: 'Overview'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - - autonomousSystem: AutonomousSystem; - - geo: Geo; - }; - - export type AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe<number>; - - organization: Maybe<Organization>; - }; - - export type Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe<string>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - city_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - country_name: Maybe<string[]>; - - location: Maybe<Location>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Location = { - __typename?: 'Location'; - - lat: Maybe<number[]>; - - lon: Maybe<number[]>; - }; - - export type Destination = { - __typename?: 'Overview'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - - autonomousSystem: _AutonomousSystem; - - geo: _Geo; - }; - - export type _AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe<number>; - - organization: Maybe<_Organization>; - }; - - export type _Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe<string>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - city_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - country_name: Maybe<string[]>; - - location: Maybe<_Location>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type _Location = { - __typename?: 'Location'; - - lat: Maybe<number[]>; - - lon: Maybe<number[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe<string[]>; - - id: Maybe<string[]>; - - ip: Maybe<string[]>; - - mac: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - - type: Maybe<string[]>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe<string[]>; - - name: Maybe<string[]>; - - platform: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiHostDetailsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiHostDetails: KpiHostDetails; - }; - - export type KpiHostDetails = { - __typename?: 'KpiHostDetailsData'; - - authSuccess: Maybe<number>; - - authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; - - authFailure: Maybe<number>; - - authFailureHistogram: Maybe<AuthFailureHistogram[]>; - - uniqueSourceIps: Maybe<number>; - - uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; - - uniqueDestinationIps: Maybe<number>; - - uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; - - inspect: Maybe<Inspect>; - }; - - export type AuthSuccessHistogram = KpiHostDetailsChartFields.Fragment; - - export type AuthFailureHistogram = KpiHostDetailsChartFields.Fragment; - - export type UniqueSourceIpsHistogram = KpiHostDetailsChartFields.Fragment; - - export type UniqueDestinationIpsHistogram = KpiHostDetailsChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiHostsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiHosts: KpiHosts; - }; - - export type KpiHosts = { - __typename?: 'KpiHostsData'; - - hosts: Maybe<number>; - - hostsHistogram: Maybe<HostsHistogram[]>; - - authSuccess: Maybe<number>; - - authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; - - authFailure: Maybe<number>; - - authFailureHistogram: Maybe<AuthFailureHistogram[]>; - - uniqueSourceIps: Maybe<number>; - - uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; - - uniqueDestinationIps: Maybe<number>; - - uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; - - inspect: Maybe<Inspect>; - }; - - export type HostsHistogram = KpiHostChartFields.Fragment; - - export type AuthSuccessHistogram = KpiHostChartFields.Fragment; - - export type AuthFailureHistogram = KpiHostChartFields.Fragment; - - export type UniqueSourceIpsHistogram = KpiHostChartFields.Fragment; - - export type UniqueDestinationIpsHistogram = KpiHostChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiNetworkQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiNetwork: Maybe<KpiNetwork>; - }; - - export type KpiNetwork = { - __typename?: 'KpiNetworkData'; - - networkEvents: Maybe<number>; - - uniqueFlowId: Maybe<number>; - - uniqueSourcePrivateIps: Maybe<number>; - - uniqueSourcePrivateIpsHistogram: Maybe<UniqueSourcePrivateIpsHistogram[]>; - - uniqueDestinationPrivateIps: Maybe<number>; - - uniqueDestinationPrivateIpsHistogram: Maybe<UniqueDestinationPrivateIpsHistogram[]>; - - dnsQueries: Maybe<number>; - - tlsHandshakes: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type UniqueSourcePrivateIpsHistogram = KpiNetworkChartFields.Fragment; - - export type UniqueDestinationPrivateIpsHistogram = KpiNetworkChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetMatrixHistogramQuery { - export type Variables = { - defaultIndex: string[]; - filterQuery?: Maybe<string>; - histogramType: HistogramType; - inspect: boolean; - sourceId: string; - stackByField: string; - timerange: TimerangeInput; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - MatrixHistogram: MatrixHistogram; - }; - - export type MatrixHistogram = { - __typename?: 'MatrixHistogramOverTimeData'; - - matrixHistogramData: MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<Inspect>; - }; - - export type MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - - g: Maybe<string>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkDnsQuery { - export type Variables = { - defaultIndex: string[]; - filterQuery?: Maybe<string>; - inspect: boolean; - isPtrIncluded: boolean; - pagination: PaginationInputPaginated; - sort: NetworkDnsSortField; - sourceId: string; - stackByField?: Maybe<string>; - timerange: TimerangeInput; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkDns: NetworkDns; - }; - - export type NetworkDns = { - __typename?: 'NetworkDnsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkDnsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkDnsItem'; - - _id: Maybe<string>; - - dnsBytesIn: Maybe<number>; - - dnsBytesOut: Maybe<number>; - - dnsName: Maybe<string>; - - queryCount: Maybe<number>; - - uniqueDomains: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkHttpQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkHttpSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkHttp: NetworkHttp; - }; - - export type NetworkHttp = { - __typename?: 'NetworkHttpData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkHttpEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkHttpItem'; - - domains: string[]; - - lastHost: Maybe<string>; - - lastSourceIp: Maybe<string>; - - methods: string[]; - - path: Maybe<string>; - - requestCount: Maybe<number>; - - statuses: string[]; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkTopCountriesQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkTopTablesSortField; - flowTarget: FlowTargetSourceDest; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkTopCountries: NetworkTopCountries; - }; - - export type NetworkTopCountries = { - __typename?: 'NetworkTopCountriesData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkTopCountriesEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkTopCountriesItem'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - network: Maybe<Network>; - }; - - export type _Source = { - __typename?: 'TopCountriesItemSource'; - - country: Maybe<string>; - - destination_ips: Maybe<number>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type Destination = { - __typename?: 'TopCountriesItemDestination'; - - country: Maybe<string>; - - destination_ips: Maybe<number>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type Network = { - __typename?: 'TopNetworkTablesEcsField'; - - bytes_in: Maybe<number>; - - bytes_out: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkTopNFlowQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkTopTablesSortField; - flowTarget: FlowTargetSourceDest; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkTopNFlow: NetworkTopNFlow; - }; - - export type NetworkTopNFlow = { - __typename?: 'NetworkTopNFlowData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkTopNFlowEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkTopNFlowItem'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - network: Maybe<Network>; - }; - - export type _Source = { - __typename?: 'TopNFlowItemSource'; - - autonomous_system: Maybe<AutonomousSystem>; - - domain: Maybe<string[]>; - - ip: Maybe<string>; - - location: Maybe<Location>; - - flows: Maybe<number>; - - destination_ips: Maybe<number>; - }; - - export type AutonomousSystem = { - __typename?: 'AutonomousSystemItem'; - - name: Maybe<string>; - - number: Maybe<number>; - }; - - export type Location = { - __typename?: 'GeoItem'; - - geo: Maybe<Geo>; - - flowTarget: Maybe<FlowTargetSourceDest>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Destination = { - __typename?: 'TopNFlowItemDestination'; - - autonomous_system: Maybe<_AutonomousSystem>; - - domain: Maybe<string[]>; - - ip: Maybe<string>; - - location: Maybe<_Location>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type _AutonomousSystem = { - __typename?: 'AutonomousSystemItem'; - - name: Maybe<string>; - - number: Maybe<number>; - }; - - export type _Location = { - __typename?: 'GeoItem'; - - geo: Maybe<_Geo>; - - flowTarget: Maybe<FlowTargetSourceDest>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Network = { - __typename?: 'TopNetworkTablesEcsField'; - - bytes_in: Maybe<number>; - - bytes_out: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetOverviewHostQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewHost: Maybe<OverviewHost>; - }; - - export type OverviewHost = { - __typename?: 'OverviewHostData'; - - auditbeatAuditd: Maybe<number>; - - auditbeatFIM: Maybe<number>; - - auditbeatLogin: Maybe<number>; - - auditbeatPackage: Maybe<number>; - - auditbeatProcess: Maybe<number>; - - auditbeatUser: Maybe<number>; - - endgameDns: Maybe<number>; - - endgameFile: Maybe<number>; - - endgameImageLoad: Maybe<number>; - - endgameNetwork: Maybe<number>; - - endgameProcess: Maybe<number>; - - endgameRegistry: Maybe<number>; - - endgameSecurity: Maybe<number>; - - filebeatSystemModule: Maybe<number>; - - winlogbeatSecurity: Maybe<number>; - - winlogbeatMWSysmonOperational: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetOverviewNetworkQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewNetwork: Maybe<OverviewNetwork>; - }; - - export type OverviewNetwork = { - __typename?: 'OverviewNetworkData'; - - auditbeatSocket: Maybe<number>; - - filebeatCisco: Maybe<number>; - - filebeatNetflow: Maybe<number>; - - filebeatPanw: Maybe<number>; - - filebeatSuricata: Maybe<number>; - - filebeatZeek: Maybe<number>; - - packetbeatDNS: Maybe<number>; - - packetbeatFlow: Maybe<number>; - - packetbeatTLS: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace SourceQuery { - export type Variables = { - sourceId?: Maybe<string>; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - status: Status; - }; - - export type Status = { - __typename?: 'SourceStatus'; - - indicesExist: boolean; - - indexFields: IndexFields[]; - }; - - export type IndexFields = { - __typename?: 'IndexField'; - - category: string; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: (Maybe<string>)[]; - - name: string; - - searchable: boolean; - - type: string; - - aggregatable: boolean; - - format: Maybe<string>; - }; -} - -export namespace GetAllTimeline { - export type Variables = { - pageInfo: PageInfoTimeline; - search?: Maybe<string>; - sort?: Maybe<SortTimeline>; - onlyUserFavorite?: Maybe<boolean>; - }; - - export type Query = { - __typename?: 'Query'; - - getAllTimeline: GetAllTimeline; - }; - - export type GetAllTimeline = { - __typename?: 'ResponseTimelines'; - - totalCount: Maybe<number>; - - timeline: (Maybe<Timeline>)[]; - }; - - export type Timeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - description: Maybe<string>; - - favorite: Maybe<Favorite[]>; - - eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; - - notes: Maybe<Notes[]>; - - noteIds: Maybe<string[]>; - - pinnedEventIds: Maybe<string[]>; - - title: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: string; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type EventIdToNoteIds = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - timelineVersion: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Notes = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace DeleteTimelineMutation { - export type Variables = { - id: string[]; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - deleteTimeline: boolean; - }; -} - -export namespace GetTimelineDetailsQuery { - export type Variables = { - sourceId: string; - eventId: string; - indexName: string; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - TimelineDetails: TimelineDetails; - }; - - export type TimelineDetails = { - __typename?: 'TimelineDetailsData'; - - data: Maybe<Data[]>; - }; - - export type Data = { - __typename?: 'DetailItem'; - - field: string; - - values: Maybe<string[]>; - - originalValue: Maybe<EsValue>; - }; -} - -export namespace PersistTimelineFavoriteMutation { - export type Variables = { - timelineId?: Maybe<string>; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistFavorite: PersistFavorite; - }; - - export type PersistFavorite = { - __typename?: 'ResponseFavoriteTimeline'; - - savedObjectId: string; - - version: string; - - favorite: Maybe<Favorite[]>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; -} - -export namespace GetTimelineQuery { - export type Variables = { - sourceId: string; - fieldRequested: string[]; - pagination: PaginationInput; - sortField: SortField; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Timeline: Timeline; - }; - - export type Timeline = { - __typename?: 'TimelineData'; - - totalCount: number; - - inspect: Maybe<Inspect>; - - pageInfo: PageInfo; - - edges: Edges[]; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type PageInfo = { - __typename?: 'PageInfo'; - - endCursor: Maybe<EndCursor>; - - hasNextPage: Maybe<boolean>; - }; - - export type EndCursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - - tiebreaker: Maybe<string>; - }; - - export type Edges = { - __typename?: 'TimelineEdges'; - - node: Node; - }; - - export type Node = { - __typename?: 'TimelineItem'; - - _id: string; - - _index: Maybe<string>; - - data: Data[]; - - ecs: Ecs; - }; - - export type Data = { - __typename?: 'TimelineNonEcsData'; - - field: string; - - value: Maybe<string[]>; - }; - - export type Ecs = { - __typename?: 'ECS'; - - _id: string; - - _index: Maybe<string>; - - timestamp: Maybe<string>; - - message: Maybe<string[]>; - - system: Maybe<System>; - - event: Maybe<Event>; - - auditd: Maybe<Auditd>; - - file: Maybe<File>; - - host: Maybe<Host>; - - rule: Maybe<Rule>; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - dns: Maybe<Dns>; - - endgame: Maybe<Endgame>; - - geo: Maybe<__Geo>; - - signal: Maybe<Signal>; - - suricata: Maybe<Suricata>; - - network: Maybe<Network>; - - http: Maybe<Http>; - - tls: Maybe<Tls>; - - url: Maybe<Url>; - - user: Maybe<User>; - - winlog: Maybe<Winlog>; - - process: Maybe<Process>; - - zeek: Maybe<Zeek>; - }; - - export type System = { - __typename?: 'SystemEcsField'; - - auth: Maybe<Auth>; - - audit: Maybe<Audit>; - }; - - export type Auth = { - __typename?: 'AuthEcsFields'; - - ssh: Maybe<Ssh>; - }; - - export type Ssh = { - __typename?: 'SshEcsFields'; - - signature: Maybe<string[]>; - - method: Maybe<string[]>; - }; - - export type Audit = { - __typename?: 'AuditEcsFields'; - - package: Maybe<Package>; - }; - - export type Package = { - __typename?: 'PackageEcsFields'; - - arch: Maybe<string[]>; - - entity_id: Maybe<string[]>; - - name: Maybe<string[]>; - - size: Maybe<number[]>; - - summary: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Event = { - __typename?: 'EventEcsFields'; - - action: Maybe<string[]>; - - category: Maybe<string[]>; - - code: Maybe<string[]>; - - created: Maybe<string[]>; - - dataset: Maybe<string[]>; - - duration: Maybe<number[]>; - - end: Maybe<string[]>; - - hash: Maybe<string[]>; - - id: Maybe<string[]>; - - kind: Maybe<string[]>; - - module: Maybe<string[]>; - - original: Maybe<string[]>; - - outcome: Maybe<string[]>; - - risk_score: Maybe<number[]>; - - risk_score_norm: Maybe<number[]>; - - severity: Maybe<number[]>; - - start: Maybe<string[]>; - - timezone: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type Auditd = { - __typename?: 'AuditdEcsFields'; - - result: Maybe<string[]>; - - session: Maybe<string[]>; - - data: Maybe<_Data>; - - summary: Maybe<Summary>; - }; - - export type _Data = { - __typename?: 'AuditdData'; - - acct: Maybe<string[]>; - - terminal: Maybe<string[]>; - - op: Maybe<string[]>; - }; - - export type Summary = { - __typename?: 'Summary'; - - actor: Maybe<Actor>; - - object: Maybe<Object>; - - how: Maybe<string[]>; - - message_type: Maybe<string[]>; - - sequence: Maybe<string[]>; - }; - - export type Actor = { - __typename?: 'PrimarySecondary'; - - primary: Maybe<string[]>; - - secondary: Maybe<string[]>; - }; - - export type Object = { - __typename?: 'PrimarySecondary'; - - primary: Maybe<string[]>; - - secondary: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type File = { - __typename?: 'FileFields'; - - name: Maybe<string[]>; - - path: Maybe<string[]>; - - target_path: Maybe<string[]>; - - extension: Maybe<string[]>; - - type: Maybe<string[]>; - - device: Maybe<string[]>; - - inode: Maybe<string[]>; - - uid: Maybe<string[]>; - - owner: Maybe<string[]>; - - gid: Maybe<string[]>; - - group: Maybe<string[]>; - - mode: Maybe<string[]>; - - size: Maybe<number[]>; - - mtime: Maybe<string[]>; - - ctime: Maybe<string[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - - ip: Maybe<string[]>; - }; - - export type Rule = { - __typename?: 'RuleEcsField'; - - reference: Maybe<string[]>; - }; - - export type _Source = { - __typename?: 'SourceEcsFields'; - - bytes: Maybe<number[]>; - - ip: Maybe<string[]>; - - packets: Maybe<number[]>; - - port: Maybe<number[]>; - - geo: Maybe<Geo>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Destination = { - __typename?: 'DestinationEcsFields'; - - bytes: Maybe<number[]>; - - ip: Maybe<string[]>; - - packets: Maybe<number[]>; - - port: Maybe<number[]>; - - geo: Maybe<_Geo>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Dns = { - __typename?: 'DnsEcsFields'; - - question: Maybe<Question>; - - resolved_ip: Maybe<string[]>; - - response_code: Maybe<string[]>; - }; - - export type Question = { - __typename?: 'DnsQuestionData'; - - name: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type Endgame = { - __typename?: 'EndgameEcsFields'; - - exit_code: Maybe<number[]>; - - file_name: Maybe<string[]>; - - file_path: Maybe<string[]>; - - logon_type: Maybe<number[]>; - - parent_process_name: Maybe<string[]>; - - pid: Maybe<number[]>; - - process_name: Maybe<string[]>; - - subject_domain_name: Maybe<string[]>; - - subject_logon_id: Maybe<string[]>; - - subject_user_name: Maybe<string[]>; - - target_domain_name: Maybe<string[]>; - - target_logon_id: Maybe<string[]>; - - target_user_name: Maybe<string[]>; - }; - - export type __Geo = { - __typename?: 'GeoEcsFields'; - - region_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - }; - - export type Signal = { - __typename?: 'SignalField'; - - original_time: Maybe<string[]>; - - rule: Maybe<_Rule>; - }; - - export type _Rule = { - __typename?: 'RuleField'; - - id: Maybe<string[]>; - - saved_id: Maybe<string[]>; - - timeline_id: Maybe<string[]>; - - timeline_title: Maybe<string[]>; - - output_index: Maybe<string[]>; - - from: Maybe<string[]>; - - index: Maybe<string[]>; - - language: Maybe<string[]>; - - query: Maybe<string[]>; - - to: Maybe<string[]>; - - filters: Maybe<ToAny>; - - note: Maybe<string[]>; - }; - - export type Suricata = { - __typename?: 'SuricataEcsFields'; - - eve: Maybe<Eve>; - }; - - export type Eve = { - __typename?: 'SuricataEveData'; - - proto: Maybe<string[]>; - - flow_id: Maybe<number[]>; - - alert: Maybe<Alert>; - }; - - export type Alert = { - __typename?: 'SuricataAlertData'; - - signature: Maybe<string[]>; - - signature_id: Maybe<number[]>; - }; - - export type Network = { - __typename?: 'NetworkEcsField'; - - bytes: Maybe<number[]>; - - community_id: Maybe<string[]>; - - direction: Maybe<string[]>; - - packets: Maybe<number[]>; - - protocol: Maybe<string[]>; - - transport: Maybe<string[]>; - }; - - export type Http = { - __typename?: 'HttpEcsFields'; - - version: Maybe<string[]>; - - request: Maybe<Request>; - - response: Maybe<Response>; - }; - - export type Request = { - __typename?: 'HttpRequestData'; - - method: Maybe<string[]>; - - body: Maybe<Body>; - - referrer: Maybe<string[]>; - }; - - export type Body = { - __typename?: 'HttpBodyData'; - - bytes: Maybe<number[]>; - - content: Maybe<string[]>; - }; - - export type Response = { - __typename?: 'HttpResponseData'; - - status_code: Maybe<number[]>; - - body: Maybe<_Body>; - }; - - export type _Body = { - __typename?: 'HttpBodyData'; - - bytes: Maybe<number[]>; - - content: Maybe<string[]>; - }; - - export type Tls = { - __typename?: 'TlsEcsFields'; - - client_certificate: Maybe<ClientCertificate>; - - fingerprints: Maybe<Fingerprints>; - - server_certificate: Maybe<ServerCertificate>; - }; - - export type ClientCertificate = { - __typename?: 'TlsClientCertificateData'; - - fingerprint: Maybe<Fingerprint>; - }; - - export type Fingerprint = { - __typename?: 'FingerprintData'; - - sha1: Maybe<string[]>; - }; - - export type Fingerprints = { - __typename?: 'TlsFingerprintsData'; - - ja3: Maybe<Ja3>; - }; - - export type Ja3 = { - __typename?: 'TlsJa3Data'; - - hash: Maybe<string[]>; - }; - - export type ServerCertificate = { - __typename?: 'TlsServerCertificateData'; - - fingerprint: Maybe<_Fingerprint>; - }; - - export type _Fingerprint = { - __typename?: 'FingerprintData'; - - sha1: Maybe<string[]>; - }; - - export type Url = { - __typename?: 'UrlEcsFields'; - - original: Maybe<string[]>; - - domain: Maybe<string[]>; - - username: Maybe<string[]>; - - password: Maybe<string[]>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - domain: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Winlog = { - __typename?: 'WinlogEcsFields'; - - event_id: Maybe<number[]>; - }; - - export type Process = { - __typename?: 'ProcessEcsFields'; - - hash: Maybe<Hash>; - - pid: Maybe<number[]>; - - name: Maybe<string[]>; - - ppid: Maybe<number[]>; - - args: Maybe<string[]>; - - executable: Maybe<string[]>; - - title: Maybe<string[]>; - - working_directory: Maybe<string[]>; - }; - - export type Hash = { - __typename?: 'ProcessHashData'; - - md5: Maybe<string[]>; - - sha1: Maybe<string[]>; - - sha256: Maybe<string[]>; - }; - - export type Zeek = { - __typename?: 'ZeekEcsFields'; - - session_id: Maybe<string[]>; - - connection: Maybe<Connection>; - - notice: Maybe<Notice>; - - dns: Maybe<_Dns>; - - http: Maybe<_Http>; - - files: Maybe<Files>; - - ssl: Maybe<Ssl>; - }; - - export type Connection = { - __typename?: 'ZeekConnectionData'; - - local_resp: Maybe<boolean[]>; - - local_orig: Maybe<boolean[]>; - - missed_bytes: Maybe<number[]>; - - state: Maybe<string[]>; - - history: Maybe<string[]>; - }; - - export type Notice = { - __typename?: 'ZeekNoticeData'; - - suppress_for: Maybe<number[]>; - - msg: Maybe<string[]>; - - note: Maybe<string[]>; - - sub: Maybe<string[]>; - - dst: Maybe<string[]>; - - dropped: Maybe<boolean[]>; - - peer_descr: Maybe<string[]>; - }; - - export type _Dns = { - __typename?: 'ZeekDnsData'; - - AA: Maybe<boolean[]>; - - qclass_name: Maybe<string[]>; - - RD: Maybe<boolean[]>; - - qtype_name: Maybe<string[]>; - - rejected: Maybe<boolean[]>; - - qtype: Maybe<string[]>; - - query: Maybe<string[]>; - - trans_id: Maybe<number[]>; - - qclass: Maybe<string[]>; - - RA: Maybe<boolean[]>; - - TC: Maybe<boolean[]>; - }; - - export type _Http = { - __typename?: 'ZeekHttpData'; - - resp_mime_types: Maybe<string[]>; - - trans_depth: Maybe<string[]>; - - status_msg: Maybe<string[]>; - - resp_fuids: Maybe<string[]>; - - tags: Maybe<string[]>; - }; - - export type Files = { - __typename?: 'ZeekFileData'; - - session_ids: Maybe<string[]>; - - timedout: Maybe<boolean[]>; - - local_orig: Maybe<boolean[]>; - - tx_host: Maybe<string[]>; - - source: Maybe<string[]>; - - is_orig: Maybe<boolean[]>; - - overflow_bytes: Maybe<number[]>; - - sha1: Maybe<string[]>; - - duration: Maybe<number[]>; - - depth: Maybe<number[]>; - - analyzers: Maybe<string[]>; - - mime_type: Maybe<string[]>; - - rx_host: Maybe<string[]>; - - total_bytes: Maybe<number[]>; - - fuid: Maybe<string[]>; - - seen_bytes: Maybe<number[]>; - - missing_bytes: Maybe<number[]>; - - md5: Maybe<string[]>; - }; - - export type Ssl = { - __typename?: 'ZeekSslData'; - - cipher: Maybe<string[]>; - - established: Maybe<boolean[]>; - - resumed: Maybe<boolean[]>; - - version: Maybe<string[]>; - }; -} - -export namespace PersistTimelineNoteMutation { - export type Variables = { - noteId?: Maybe<string>; - version?: Maybe<string>; - note: NoteInput; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistNote: PersistNote; - }; - - export type PersistNote = { - __typename?: 'ResponseNote'; - - code: Maybe<number>; - - message: Maybe<string>; - - note: Note; - }; - - export type Note = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace GetOneTimeline { - export type Variables = { - id: string; - }; - - export type Query = { - __typename?: 'Query'; - - getOneTimeline: GetOneTimeline; - }; - - export type GetOneTimeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - columns: Maybe<Columns[]>; - - dataProviders: Maybe<DataProviders[]>; - - dateRange: Maybe<DateRange>; - - description: Maybe<string>; - - eventType: Maybe<string>; - - eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; - - favorite: Maybe<Favorite[]>; - - filters: Maybe<Filters[]>; - - kqlMode: Maybe<string>; - - kqlQuery: Maybe<KqlQuery>; - - notes: Maybe<Notes[]>; - - noteIds: Maybe<string[]>; - - pinnedEventIds: Maybe<string[]>; - - pinnedEventsSaveObject: Maybe<PinnedEventsSaveObject[]>; - - title: Maybe<string>; - - savedQueryId: Maybe<string>; - - sort: Maybe<Sort>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: string; - }; - - export type Columns = { - __typename?: 'ColumnHeaderResult'; - - aggregatable: Maybe<boolean>; - - category: Maybe<string>; - - columnHeaderType: Maybe<string>; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: Maybe<string[]>; - - id: Maybe<string>; - - name: Maybe<string>; - - searchable: Maybe<boolean>; - - type: Maybe<string>; - }; - - export type DataProviders = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<QueryMatch>; - - and: Maybe<And[]>; - }; - - export type QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type And = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<_QueryMatch>; - }; - - export type _QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type DateRange = { - __typename?: 'DateRangePickerResult'; - - start: Maybe<number>; - - end: Maybe<number>; - }; - - export type EventIdToNoteIds = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - timelineVersion: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type Filters = { - __typename?: 'FilterTimelineResult'; - - meta: Maybe<Meta>; - - query: Maybe<string>; - - exists: Maybe<string>; - - match_all: Maybe<string>; - - missing: Maybe<string>; - - range: Maybe<string>; - - script: Maybe<string>; - }; - - export type Meta = { - __typename?: 'FilterMetaTimelineResult'; - - alias: Maybe<string>; - - controlledBy: Maybe<string>; - - disabled: Maybe<boolean>; - - field: Maybe<string>; - - formattedValue: Maybe<string>; - - index: Maybe<string>; - - key: Maybe<string>; - - negate: Maybe<boolean>; - - params: Maybe<string>; - - type: Maybe<string>; - - value: Maybe<string>; - }; - - export type KqlQuery = { - __typename?: 'SerializedFilterQueryResult'; - - filterQuery: Maybe<FilterQuery>; - }; - - export type FilterQuery = { - __typename?: 'SerializedKueryQueryResult'; - - kuery: Maybe<Kuery>; - - serializedQuery: Maybe<string>; - }; - - export type Kuery = { - __typename?: 'KueryFilterQueryResult'; - - kind: Maybe<string>; - - expression: Maybe<string>; - }; - - export type Notes = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type PinnedEventsSaveObject = { - __typename?: 'PinnedEvent'; - - pinnedEventId: string; - - eventId: Maybe<string>; - - timelineId: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe<string>; - - sortDirection: Maybe<string>; - }; -} - -export namespace PersistTimelineMutation { - export type Variables = { - timelineId?: Maybe<string>; - version?: Maybe<string>; - timeline: TimelineInput; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistTimeline: PersistTimeline; - }; - - export type PersistTimeline = { - __typename?: 'ResponseTimeline'; - - code: Maybe<number>; - - message: Maybe<string>; - - timeline: Timeline; - }; - - export type Timeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - version: string; - - columns: Maybe<Columns[]>; - - dataProviders: Maybe<DataProviders[]>; - - description: Maybe<string>; - - eventType: Maybe<string>; - - favorite: Maybe<Favorite[]>; - - filters: Maybe<Filters[]>; - - kqlMode: Maybe<string>; - - kqlQuery: Maybe<KqlQuery>; - - title: Maybe<string>; - - dateRange: Maybe<DateRange>; - - savedQueryId: Maybe<string>; - - sort: Maybe<Sort>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - }; - - export type Columns = { - __typename?: 'ColumnHeaderResult'; - - aggregatable: Maybe<boolean>; - - category: Maybe<string>; - - columnHeaderType: Maybe<string>; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: Maybe<string[]>; - - id: Maybe<string>; - - name: Maybe<string>; - - searchable: Maybe<boolean>; - - type: Maybe<string>; - }; - - export type DataProviders = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<QueryMatch>; - - and: Maybe<And[]>; - }; - - export type QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type And = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<_QueryMatch>; - }; - - export type _QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type Filters = { - __typename?: 'FilterTimelineResult'; - - meta: Maybe<Meta>; - - query: Maybe<string>; - - exists: Maybe<string>; - - match_all: Maybe<string>; - - missing: Maybe<string>; - - range: Maybe<string>; - - script: Maybe<string>; - }; - - export type Meta = { - __typename?: 'FilterMetaTimelineResult'; - - alias: Maybe<string>; - - controlledBy: Maybe<string>; - - disabled: Maybe<boolean>; - - field: Maybe<string>; - - formattedValue: Maybe<string>; - - index: Maybe<string>; - - key: Maybe<string>; - - negate: Maybe<boolean>; - - params: Maybe<string>; - - type: Maybe<string>; - - value: Maybe<string>; - }; - - export type KqlQuery = { - __typename?: 'SerializedFilterQueryResult'; - - filterQuery: Maybe<FilterQuery>; - }; - - export type FilterQuery = { - __typename?: 'SerializedKueryQueryResult'; - - kuery: Maybe<Kuery>; - - serializedQuery: Maybe<string>; - }; - - export type Kuery = { - __typename?: 'KueryFilterQueryResult'; - - kind: Maybe<string>; - - expression: Maybe<string>; - }; - - export type DateRange = { - __typename?: 'DateRangePickerResult'; - - start: Maybe<number>; - - end: Maybe<number>; - }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe<string>; - - sortDirection: Maybe<string>; - }; -} - -export namespace PersistTimelinePinnedEventMutation { - export type Variables = { - pinnedEventId?: Maybe<string>; - eventId: string; - timelineId?: Maybe<string>; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistPinnedEventOnTimeline: Maybe<PersistPinnedEventOnTimeline>; - }; - - export type PersistPinnedEventOnTimeline = { - __typename?: 'PinnedEvent'; - - pinnedEventId: string; - - eventId: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace GetTlsQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - flowTarget: FlowTargetSourceDest; - ip: string; - pagination: PaginationInputPaginated; - sort: TlsSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Tls: Tls; - }; - - export type Tls = { - __typename?: 'TlsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'TlsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'TlsNode'; - - _id: Maybe<string>; - - subjects: Maybe<string[]>; - - ja3: Maybe<string[]>; - - issuers: Maybe<string[]>; - - notAfter: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetUncommonProcessesQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - UncommonProcesses: UncommonProcesses; - }; - - export type UncommonProcesses = { - __typename?: 'UncommonProcessesData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'UncommonProcessesEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UncommonProcessItem'; - - _id: string; - - instances: number; - - process: Process; - - user: Maybe<User>; - - hosts: Hosts[]; - }; - - export type Process = { - __typename?: 'ProcessEcsFields'; - - args: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Hosts = { - __typename?: 'HostEcsFields'; - - name: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetUsersQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - flowTarget: FlowTarget; - ip: string; - pagination: PaginationInputPaginated; - sort: UsersSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Users: Users; - }; - - export type Users = { - __typename?: 'UsersData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'UsersEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UsersNode'; - - user: Maybe<User>; - }; - - export type User = { - __typename?: 'UsersItem'; - - name: Maybe<string>; - - id: Maybe<string[]>; - - groupId: Maybe<string[]>; - - groupName: Maybe<string[]>; - - count: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace KpiHostDetailsChartFields { - export type Fragment = { - __typename?: 'KpiHostHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} - -export namespace KpiHostChartFields { - export type Fragment = { - __typename?: 'KpiHostHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} - -export namespace KpiNetworkChartFields { - export type Fragment = { - __typename?: 'KpiNetworkHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts deleted file mode 100644 index 301b8bd655333..0000000000000 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SimpleSavedObject } from '../../../../../../src/core/public'; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type IndexPatternSavedObjectAttributes = { title: string }; - -export type IndexPatternSavedObject = Pick< - SimpleSavedObject<IndexPatternSavedObjectAttributes>, - 'type' | 'id' | 'attributes' | '_version' ->; diff --git a/x-pack/legacy/plugins/siem/public/index.ts b/x-pack/legacy/plugins/siem/public/index.ts deleted file mode 100644 index 3a396a0637ea1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin, PluginInitializerContext } from './plugin'; - -export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); diff --git a/x-pack/legacy/plugins/siem/public/legacy.ts b/x-pack/legacy/plugins/siem/public/legacy.ts deleted file mode 100644 index b3a06a170bb80..0000000000000 --- a/x-pack/legacy/plugins/siem/public/legacy.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup, npStart } from 'ui/new_platform'; - -import { PluginInitializerContext } from '../../../../../src/core/public'; -import { plugin } from './'; -import { SetupPlugins, StartPlugins } from './plugin'; - -const pluginInstance = plugin({} as PluginInitializerContext); - -pluginInstance.setup(npSetup.core, (npSetup.plugins as unknown) as SetupPlugins); -pluginInstance.start(npStart.core, (npStart.plugins as unknown) as StartPlugins); diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts deleted file mode 100644 index 66326a6590deb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ConfigType, - SecretsType, -} from '../../../../../../plugins/actions/server/builtin_action_types/servicenow/types'; - -export interface ServiceNowActionConnector { - config: ConfigType; - secrets: SecretsType; -} - -export interface Connector { - actionTypeId: string; - logo: string; -} diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts deleted file mode 100644 index 53f845de48fb3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty, isString, flow } from 'lodash/fp'; -import { - EsQueryConfig, - Query, - Filter, - esQuery, - esKuery, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; - -import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; - -import { KueryFilterQuery } from '../../store'; - -export const convertKueryToElasticSearchQuery = ( - kueryExpression: string, - indexPattern?: IIndexPattern -) => { - try { - return kueryExpression - ? JSON.stringify( - esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - ) - : ''; - } catch (err) { - return ''; - } -}; - -export const convertKueryToDslFilter = ( - kueryExpression: string, - indexPattern: IIndexPattern -): JsonObject => { - try { - return kueryExpression - ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - : {}; - } catch (err) { - return {}; - } -}; - -export const escapeQueryValue = (val: number | string = ''): string | number => { - if (isString(val)) { - if (isEmpty(val)) { - return '""'; - } - return `"${escapeKuery(val)}"`; - } - - return val; -}; - -export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { - if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { - try { - esKuery.fromKueryExpression(kqlFilterQuery.expression); - } catch (err) { - return false; - } - } - return true; -}; - -const escapeWhitespace = (val: string) => - val - .replace(/\t/g, '\\t') - .replace(/\r/g, '\\r') - .replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); - -export const convertToBuildEsQuery = ({ - config, - indexPattern, - queries, - filters, -}: { - config: EsQueryConfig; - indexPattern: IIndexPattern; - queries: Query[]; - filters: Filter[]; -}) => { - try { - return JSON.stringify( - esQuery.buildEsQuery( - indexPattern, - queries, - filters.filter(f => f.meta.disabled === false), - { - ...config, - dateFormatTZ: undefined, - } - ) - ); - } catch (exp) { - return ''; - } -}; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts deleted file mode 100644 index 227680d79912f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - createKibanaContextProviderMock, - createUseUiSettingMock, - createUseUiSetting$Mock, - createUseKibanaMock, - createWithKibanaMock, -} from '../../../mock/kibana_react'; - -export const KibanaServices = { get: jest.fn() }; -export const useKibana = jest.fn(createUseKibanaMock()); -export const useUiSetting = jest.fn(createUseUiSettingMock()); -export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); -export const useTimeZone = jest.fn(); -export const useDateFormat = jest.fn(); -export const useBasePath = jest.fn(() => '/test/base/path'); -export const withKibana = jest.fn(createWithKibanaMock()); -export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts deleted file mode 100644 index 012a1cfef5da2..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - KibanaContextProvider, - KibanaReactContextValue, - useKibana, - useUiSetting, - useUiSetting$, - withKibana, -} from '../../../../../../../src/plugins/kibana_react/public'; -import { StartServices } from '../../plugin'; - -export type KibanaContext = KibanaReactContextValue<StartServices>; -export interface WithKibanaProps { - kibana: KibanaContext; -} - -// eslint-disable-next-line react-hooks/rules-of-hooks -const typedUseKibana = () => useKibana<StartServices>(); - -export { - KibanaContextProvider, - typedUseKibana as useKibana, - useUiSetting, - useUiSetting$, - withKibana, -}; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts deleted file mode 100644 index 3a6a3f13dc5ce..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { StartServices } from '../../plugin'; - -type GlobalServices = Pick<StartServices, 'http' | 'uiSettings'>; - -export class KibanaServices { - private static services?: GlobalServices; - - public static init({ http, uiSettings }: StartServices) { - this.services = { http, uiSettings }; - } - - public static get(): GlobalServices { - if (!this.services) { - throw new Error( - 'Kibana services not set - are you trying to import this module from outside of the SIEM app?' - ); - } - - return this.services; - } -} diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts deleted file mode 100644 index cf3523a6260bb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; - -export const createKibanaCoreSetupMock = () => createUiNewPlatformMock().npSetup.core; -export const createKibanaPluginsSetupMock = () => createUiNewPlatformMock().npSetup.plugins; - -export const createKibanaCoreStartMock = () => createUiNewPlatformMock().npStart.core; -export const createKibanaPluginsStartMock = () => createUiNewPlatformMock().npStart.plugins; diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts deleted file mode 100644 index db7a931b3fb15..0000000000000 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; - -import { - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, - DEFAULT_FROM, - DEFAULT_TO, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, -} from '../../../../../plugins/siem/common/constants'; -import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockUiSettings: Record<string, any> = { - [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, - [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, - [DEFAULT_SIEM_TIME_RANGE]: { - from: DEFAULT_FROM, - to: DEFAULT_TO, - }, - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - pause: DEFAULT_INTERVAL_PAUSE, - value: DEFAULT_INTERVAL_VALUE, - }, - [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, - [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', - [DEFAULT_DARK_MODE]: false, -}; - -export const createUseUiSettingMock = () => <T extends unknown = string>( - key: string, - defaultValue?: T -): T => { - const result = mockUiSettings[key]; - - if (typeof result != null) return result; - - if (defaultValue != null) { - return defaultValue; - } - - throw new Error(`Unexpected config key: ${key}`); -}; - -export const createUseUiSetting$Mock = () => { - const useUiSettingMock = createUseUiSettingMock(); - - return <T extends unknown = string>( - key: string, - defaultValue?: T - ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; -}; - -export const createUseKibanaMock = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - const useUiSetting = createUseUiSettingMock(); - - const services = { - ...core, - ...plugins, - uiSettings: { - ...core.uiSettings, - get: useUiSetting, - }, - }; - - return () => ({ services }); -}; - -export const createWithKibanaMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: any) => { - return React.createElement(Component, { ...props, kibana }); - }; -}; - -export const createKibanaContextProviderMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ({ services, ...rest }: any) => - React.createElement(KibanaContextProvider, { - ...rest, - services: { ...kibana.services, ...services }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx deleted file mode 100644 index 74f6411f17fa0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { AddComment } from './'; -import { TestProviders } from '../../../../mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; - -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { wait } from '../../../../lib/helpers'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_post_comment'); - -export const useFormMock = useForm as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; -const usePostCommentMock = usePostComment as jest.Mock; - -const onCommentSaving = jest.fn(); -const onCommentPosted = jest.fn(); -const postComment = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const addCommentProps = { - caseId: '1234', - disabled: false, - insertQuote: null, - onCommentSaving, - onCommentPosted, - showLoading: false, -}; - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - -const defaultPostCommment = { - isLoading: false, - isError: false, - postComment, -}; -const sampleData = { - comment: 'what a cool comment', -}; -describe('AddComment ', () => { - const formHookMock = getFormMock(sampleData); - - beforeEach(() => { - jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); - usePostCommentMock.mockImplementation(() => defaultPostCommment); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('should post comment on submit click', async () => { - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...addCommentProps} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); - - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .simulate('click'); - await wait(); - expect(onCommentSaving).toBeCalled(); - expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); - }); - - it('should render spinner and disable submit when loading', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, showLoading: true }} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should disable submit button when disabled prop passed', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, disabled: true }} /> - </Router> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should insert a quote if one is available', () => { - const sampleQuote = 'what a cool quote'; - mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, insertQuote: sampleQuote }} /> - </Router> - </TestProviders> - ); - - expect(formHookMock.setFieldValue).toBeCalledWith( - 'comment', - `${sampleData.comment}\n\n${sampleQuote}` - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx deleted file mode 100644 index eaba708948a99..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import styled from 'styled-components'; - -import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { Case } from '../../../../containers/case/types'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { Form, useForm, UseField } from '../../../../shared_imports'; - -import * as i18n from '../../translations'; -import { schema } from './schema'; - -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; -`; - -const initialCommentValue: CommentRequest = { - comment: '', -}; - -interface AddCommentProps { - caseId: string; - disabled?: boolean; - insertQuote: string | null; - onCommentSaving?: () => void; - onCommentPosted: (newCase: Case) => void; - showLoading?: boolean; -} - -export const AddComment = React.memo<AddCommentProps>( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm<CommentRequest>({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>( - form, - 'comment' - ); - - useEffect(() => { - if (insertQuote !== null) { - const { comment } = form.getFormData(); - form.setFieldValue( - 'comment', - `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` - ); - } - }, [insertQuote]); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); - } - await postComment(data, onCommentPosted); - form.reset(); - } - }, [form, onCommentPosted, onCommentSaving]); - return ( - <span id="add-comment-permLink"> - {isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />} - <Form form={form}> - <UseField - path="comment" - component={MarkdownEditorForm} - componentProps={{ - idAria: 'caseComment', - isDisabled: isLoading, - dataTestSubj: 'add-comment', - placeholder: i18n.ADD_COMMENT_HELP_TEXT, - onCursorPositionUpdate: handleCursorChange, - bottomRightContent: ( - <EuiButton - data-test-subj="submit-comment" - iconType="plusInCircle" - isDisabled={isLoading || disabled} - isLoading={isLoading} - onClick={onSubmit} - size="s" - > - {i18n.ADD_COMMENT} - </EuiButton> - ), - topRightContent: ( - <InsertTimelinePopover - hideUntitled={true} - isDisabled={isLoading} - onTimelineChange={handleOnTimelineChange} - /> - ), - }} - /> - </Form> - </span> - ); - } -); - -AddComment.displayName = 'AddComment'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx deleted file mode 100644 index c61874a8dabfc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -const { emptyField } = fieldValidators; - -export const schema: FormSchema<CommentRequest> = { - comment: { - type: FIELD_TYPES.TEXTAREA, - validations: [ - { - validator: emptyField(i18n.COMMENT_REQUIRED), - }, - ], - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx deleted file mode 100644 index 135f0f2a7e26d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Connector, - CasesConfigurationMapping, -} from '../../../../../containers/case/configure/types'; -import { State } from '../reducer'; -import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; -import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; -import { createUseKibanaMock } from '../../../../../mock/kibana_react'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { actionTypeRegistryMock } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/action_type_registry.mock'; - -export const connectors: Connector[] = [ - { - id: '123', - actionTypeId: '.servicenow', - name: 'My Connector', - isPreconfigured: false, - config: { - apiUrl: 'https://instance1.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - }, - { - id: '456', - actionTypeId: '.servicenow', - name: 'My Connector 2', - isPreconfigured: false, - config: { - apiUrl: 'https://instance2.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - }, -]; - -export const mapping: CasesConfigurationMapping[] = [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - -export const searchURL = - '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; - -export const initialState: State = { - connectorId: 'none', - closureType: 'close-by-user', - mapping: null, - currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, -}; - -export const useCaseConfigureResponse: ReturnUseCaseConfigure = { - loading: false, - persistLoading: false, - refetchCaseConfigure: jest.fn(), - persistCaseConfigure: jest.fn(), -}; - -export const useConnectorsResponse: ReturnConnectors = { - loading: false, - connectors, - refetchConnectors: jest.fn(), -}; - -export const kibanaMockImplementationArgs = { - services: { - ...createUseKibanaMock()().services, - triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx deleted file mode 100644 index 5ea3f500c0349..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ /dev/null @@ -1,748 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { ReactWrapper, mount } from 'enzyme'; - -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; - -import { - connectors, - searchURL, - useCaseConfigureResponse, - useConnectorsResponse, - kibanaMockImplementationArgs, -} from './__mock__'; - -jest.mock('../../../../lib/kibana'); -jest.mock('../../../../containers/case/configure/use_connectors'); -jest.mock('../../../../containers/case/configure/use_configure'); -jest.mock('../../../../components/navigation/use_get_url_search'); - -const useKibanaMock = useKibana as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; -const useCaseConfigureMock = useCaseConfigure as jest.Mock; -const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; - -import { ConfigureCases } from './'; -import { TestProviders } from '../../../../mock'; -import { Connectors } from './connectors'; -import { ClosureOptions } from './closure_options'; -import { Mapping } from './mapping'; -import { - ActionsConnectorsContextProvider, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../../../plugins/triggers_actions_ui/public'; -import { EuiBottomBar } from '@elastic/eui'; - -describe('rendering', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - }); - - test('it renders the Connectors', () => { - expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); - }); - - test('it renders the ClosureType', () => { - expect( - wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() - ).toBeTruthy(); - }); - - test('it renders the Mapping', () => { - expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); - }); - - test('it renders the ActionsConnectorsContextProvider', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); - }); - - test('it renders the ConnectorAddFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); - }); - - test('it does NOT render the ConnectorEditFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); - }); - - test('it does NOT render the EuiCallOut', () => { - expect(wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()).toBeFalsy(); - }); - - test('it does NOT render the EuiBottomBar', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); -}); - -describe('ConfigureCases - Unhappy path', () => { - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - }); - - test('it shows the warning callout when configuration is invalid', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('not-id'), []); - return useCaseConfigureResponse; - } - ); - - const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() - ).toBeTruthy(); - }); -}); - -describe('ConfigureCases - Happy path', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('123'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - }); - - test('it renders the ConnectorEditFlyout', () => { - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); - }); - - test('it renders with correct props', () => { - // Connector - expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); - expect(wrapper.find(Connectors).prop('disabled')).toBe(false); - expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); - expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); - - // ClosureOptions - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); - expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); - - // Mapping - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - - // Flyouts - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, - ]); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); - }); - - test('it disables correctly when the user cannot crud', () => { - const newWrapper = mount(<ConfigureCases userCanCrud={false} />, { - wrappingComponent: TestProviders, - }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); - }); - - test('it disables correctly Connector when loading connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - }); - - test('it disables correctly Connector when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - }); - - test('it pass the correct value to isLoading attribute on Connector', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('isLoading')).toBe(true); - }); - - test('it set correctly the selected connector', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('selectedConnector')).toBe('456'); - }); - - test('it show the add flyout when pressing the add connector button', () => { - wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); - }); - - test('it disables correctly ClosureOptions when loading connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables correctly ClosureOptions when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables correctly ClosureOptions when the connector is set to none', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('none'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables the mapping permanently', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when loading the connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when loading the configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when saving the configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when the connectorId is invalid', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('not-id'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when the connectorId is set to none', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('none'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it show the edit flyout when pressing the update connector button', () => { - wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); - }); - - test('it sets the mapping of a connector correctly', () => { - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - }); - - // TODO: When mapping is enabled the test.todo should be implemented. - test.todo('the mapping is changed successfully when changing the third party'); - test.todo('the mapping is changed successfully when changing the action type'); - - test('it does not shows the action bar when there is no change', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it shows the action bar when the connector is changed', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it shows the action bar when the closure type is changed', () => { - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it tracks the changes successfully', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks and reverts the changes successfully ', () => { - // change settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // revert back to initial settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-user"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it close and restores the action bar when the add connector button is pressed', () => { - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press add connector button - wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - - // Close the add flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it close and restores the action bar when the update connector button is pressed', () => { - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press update connector button - wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - - // Close the edit flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it disables the buttons of action bar when loading connectors', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when loading configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, loading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when saving configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistLoading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it shows the loading spinner when saving configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistLoading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isLoading') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isLoading') - ).toBe(true); - }); - - test('it closes the action bar when pressing save', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - newWrapper.update(); - - expect( - newWrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it submits the configuration correctly', () => { - const persistCaseConfigure = jest.fn(); - - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-pushing', - }), - [] - ); - return { ...useCaseConfigureResponse, persistCaseConfigure }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - newWrapper.update(); - - expect(persistCaseConfigure).toHaveBeenCalled(); - expect(persistCaseConfigure).toHaveBeenCalledWith({ - connectorId: '456', - connectorName: 'My Connector 2', - closureType: 'close-by-user', - }); - }); - - test('it has the correct url on cancel button', () => { - const persistCaseConfigure = jest.fn(); - - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistCaseConfigure }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('href') - ).toBe(`#/link-to/case${searchURL}`); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx deleted file mode 100644 index 241dcef14a145..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { - useReducer, - useCallback, - useEffect, - useState, - Dispatch, - SetStateAction, -} from 'react'; -import styled, { css } from 'styled-components'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, - EuiBottomBar, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; -import { isEmpty, difference } from 'lodash/fp'; -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { - ActionsConnectorsContextProvider, - ActionType, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../../../plugins/triggers_actions_ui/public'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorTableItem } from '../../../../../../../../plugins/triggers_actions_ui/public/types'; -import { getCaseUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { - ClosureType, - CasesConfigurationMapping, - CCMapsCombinedActionAttributes, -} from '../../../../containers/case/configure/types'; -import { Connectors } from '../configure_cases/connectors'; -import { ClosureOptions } from '../configure_cases/closure_options'; -import { Mapping } from '../configure_cases/mapping'; -import { SectionWrapper } from '../wrappers'; -import { navTabs } from '../../../../pages/home/home_navigations'; -import { configureCasesReducer, State, CurrentConfiguration } from './reducer'; -import * as i18n from './translations'; - -const FormWrapper = styled.div` - ${({ theme }) => css` - & > * { - margin-top 40px; - } - - & > :first-child { - margin-top: 0; - } - - padding-top: ${theme.eui.paddingSizes.xl}; - padding-bottom: ${theme.eui.paddingSizes.xl}; - `} -`; - -const initialState: State = { - connectorId: 'none', - closureType: 'close-by-user', - mapping: null, - currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, -}; - -const actionTypes: ActionType[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, -]; - -interface ConfigureCasesComponentProps { - userCanCrud: boolean; -} - -const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userCanCrud }) => { - const search = useGetUrlSearch(navTabs.case); - const { http, triggers_actions_ui, notifications, application } = useKibana().services; - - const [connectorIsValid, setConnectorIsValid] = useState(true); - const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); - const [editedConnectorItem, setEditedConnectorItem] = useState<ActionConnectorTableItem | null>( - null - ); - - const [actionBarVisible, setActionBarVisible] = useState(false); - const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); - - const [{ connectorId, closureType, mapping, currentConfiguration }, dispatch] = useReducer( - configureCasesReducer(), - initialState - ); - - const setCurrentConfiguration = useCallback((configuration: CurrentConfiguration) => { - dispatch({ - type: 'setCurrentConfiguration', - currentConfiguration: { ...configuration }, - }); - }, []); - - const setConnectorId = useCallback((newConnectorId: string) => { - dispatch({ - type: 'setConnectorId', - connectorId: newConnectorId, - }); - }, []); - - const setClosureType = useCallback((newClosureType: ClosureType) => { - dispatch({ - type: 'setClosureType', - closureType: newClosureType, - }); - }, []); - - const setMapping = useCallback((newMapping: CasesConfigurationMapping[]) => { - dispatch({ - type: 'setMapping', - mapping: newMapping, - }); - }, []); - - const { loading: loadingCaseConfigure, persistLoading, persistCaseConfigure } = useCaseConfigure({ - setConnector: setConnectorId, - setClosureType, - setCurrentConfiguration, - }); - - const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); - - // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise<void>. - // TODO: Fix it if reloadConnectors type change. - const reloadConnectors = useCallback(async () => refetchConnectors(), []); - const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; - const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; - - const handleSubmit = useCallback( - // TO DO give a warning/error to user when field are not mapped so they have chance to do it - () => { - setActionBarVisible(false); - persistCaseConfigure({ - connectorId, - connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', - closureType, - }); - }, - [connectorId, connectors, closureType, mapping] - ); - - const onClickAddConnector = useCallback(() => { - setActionBarVisible(false); - setAddFlyoutVisibility(true); - }, []); - - const onClickUpdateConnector = useCallback(() => { - setActionBarVisible(false); - setEditFlyoutVisibility(true); - }, []); - - const handleActionBar = useCallback(() => { - const unsavedChanges = difference(Object.values(currentConfiguration), [ - connectorId, - closureType, - ]).length; - - if (unsavedChanges === 0) { - setActionBarVisible(false); - } else { - setActionBarVisible(true); - } - - setTotalConfigurationChanges(unsavedChanges); - }, [currentConfiguration, connectorId, closureType]); - - const handleSetAddFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setAddFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - const handleSetEditFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setEditFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - useEffect(() => { - if ( - !isEmpty(connectors) && - connectorId !== 'none' && - connectors.some(c => c.id === connectorId) - ) { - const myConnector = connectors.find(c => c.id === connectorId); - const myMapping = myConnector?.config?.casesConfiguration?.mapping ?? []; - setMapping( - myMapping.map((m: CCMapsCombinedActionAttributes) => ({ - source: m.source, - target: m.target, - actionType: m.action_type ?? m.actionType, - })) - ); - } - }, [connectors, connectorId]); - - useEffect(() => { - if ( - !isLoadingConnectors && - connectorId !== 'none' && - !connectors.some(c => c.id === connectorId) - ) { - setConnectorIsValid(false); - } else if ( - !isLoadingConnectors && - (connectorId === 'none' || connectors.some(c => c.id === connectorId)) - ) { - setConnectorIsValid(true); - } - }, [connectors, connectorId]); - - useEffect(() => { - if (!isLoadingConnectors && connectorId !== 'none') { - setEditedConnectorItem( - connectors.find(c => c.id === connectorId) as ActionConnectorTableItem - ); - } - }, [connectors, connectorId]); - - useEffect(() => { - handleActionBar(); - }, [connectors, connectorId, closureType, currentConfiguration]); - - return ( - <FormWrapper> - {!connectorIsValid && ( - <SectionWrapper style={{ marginTop: 0 }}> - <EuiCallOut - title={i18n.WARNING_NO_CONNECTOR_TITLE} - color="warning" - iconType="help" - data-test-subj="configure-cases-warning-callout" - > - {i18n.WARNING_NO_CONNECTOR_MESSAGE} - </EuiCallOut> - </SectionWrapper> - )} - <SectionWrapper> - <Connectors - connectors={connectors ?? []} - disabled={persistLoading || isLoadingConnectors || !userCanCrud} - isLoading={isLoadingConnectors} - onChangeConnector={setConnectorId} - handleShowAddFlyout={onClickAddConnector} - selectedConnector={connectorId} - /> - </SectionWrapper> - <SectionWrapper> - <ClosureOptions - closureTypeSelected={closureType} - disabled={persistLoading || isLoadingConnectors || connectorId === 'none' || !userCanCrud} - onChangeClosureType={setClosureType} - /> - </SectionWrapper> - <SectionWrapper> - <Mapping - disabled - updateConnectorDisabled={updateConnectorDisabled || !userCanCrud} - mapping={mapping} - onChangeMapping={setMapping} - setEditFlyoutVisibility={onClickUpdateConnector} - /> - </SectionWrapper> - {actionBarVisible && ( - <EuiBottomBar data-test-subj="case-configure-action-bottom-bar"> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiText data-test-subj="case-configure-action-bottom-bar-total-changes"> - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} - </EuiText> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - color="ghost" - iconType="cross" - isDisabled={isLoadingAny} - isLoading={persistLoading} - aria-label={i18n.CANCEL} - href={getCaseUrl(search)} - data-test-subj="case-configure-action-bottom-bar-cancel-button" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - color="secondary" - iconType="save" - aria-label={i18n.SAVE_CHANGES} - isDisabled={isLoadingAny} - isLoading={persistLoading} - onClick={handleSubmit} - data-test-subj="case-configure-action-bottom-bar-save-button" - > - {i18n.SAVE_CHANGES} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiBottomBar> - )} - <ActionsConnectorsContextProvider - value={{ - http, - actionTypeRegistry: triggers_actions_ui.actionTypeRegistry, - toastNotifications: notifications.toasts, - capabilities: application.capabilities, - reloadConnectors, - }} - > - <ConnectorAddFlyout - addFlyoutVisible={addFlyoutVisible} - setAddFlyoutVisibility={handleSetAddFlyoutVisibility as Dispatch<SetStateAction<boolean>>} - actionTypes={actionTypes} - /> - {editedConnectorItem && ( - <ConnectorEditFlyout - key={editedConnectorItem.id} - initialConnector={editedConnectorItem} - editFlyoutVisible={editFlyoutVisible} - setEditFlyoutVisibility={ - handleSetEditFlyoutVisibility as Dispatch<SetStateAction<boolean>> - } - /> - )} - </ActionsConnectorsContextProvider> - </FormWrapper> - ); -}; - -export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts deleted file mode 100644 index df958b75dc6b8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { configureCasesReducer, Action, State } from './reducer'; -import { initialState, mapping } from './__mock__'; - -describe('Reducer', () => { - let reducer: (state: State, action: Action) => State; - - beforeAll(() => { - reducer = configureCasesReducer(); - }); - - test('it should set the correct configuration', () => { - const action: Action = { - type: 'setCurrentConfiguration', - currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - currentConfiguration: action.currentConfiguration, - }); - }); - - test('it should set the correct connector id', () => { - const action: Action = { - type: 'setConnectorId', - connectorId: '456', - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - connectorId: action.connectorId, - }); - }); - - test('it should set the closure type', () => { - const action: Action = { - type: 'setClosureType', - closureType: 'close-by-pushing', - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - closureType: action.closureType, - }); - }); - - test('it should set the mapping', () => { - const action: Action = { - type: 'setMapping', - mapping, - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - mapping: action.mapping, - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts deleted file mode 100644 index f6b9d38a76de3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ClosureType, - CasesConfigurationMapping, -} from '../../../../containers/case/configure/types'; - -export interface State { - mapping: CasesConfigurationMapping[] | null; - connectorId: string; - closureType: ClosureType; - currentConfiguration: CurrentConfiguration; -} - -export interface CurrentConfiguration { - connectorId: State['connectorId']; - closureType: State['closureType']; -} - -export type Action = - | { - type: 'setCurrentConfiguration'; - currentConfiguration: CurrentConfiguration; - } - | { - type: 'setConnectorId'; - connectorId: string; - } - | { - type: 'setClosureType'; - closureType: ClosureType; - } - | { - type: 'setMapping'; - mapping: CasesConfigurationMapping[]; - }; - -export const configureCasesReducer = () => (state: State, action: Action) => { - switch (action.type) { - case 'setCurrentConfiguration': { - return { - ...state, - currentConfiguration: { ...action.currentConfiguration }, - }; - } - case 'setConnectorId': { - return { - ...state, - connectorId: action.connectorId, - }; - } - case 'setClosureType': { - return { - ...state, - closureType: action.closureType, - }; - } - case 'setMapping': { - return { - ...state, - mapping: action.mapping, - }; - } - default: - return state; - } -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx deleted file mode 100644 index 0897be6310fa2..0000000000000 --- 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 0f819f961b396..0000000000000 --- 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 4653dbc67d5a1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CasePostRequest } from '../../../../../../../../plugins/case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -import { OptionalFieldLabel } from './optional_field_label'; -const { emptyField } = fieldValidators; - -export const schemaTags = { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.TAGS, - helpText: i18n.TAGS_HELP, - labelAppend: OptionalFieldLabel, -}; - -export const schema: FormSchema<CasePostRequest> = { - title: { - type: FIELD_TYPES.TEXT, - label: i18n.NAME, - validations: [ - { - validator: emptyField(i18n.TITLE_REQUIRED), - }, - ], - }, - description: { - label: i18n.DESCRIPTION, - validations: [ - { - validator: emptyField(i18n.DESCRIPTION_REQUIRED), - }, - ], - }, - tags: schemaTags, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/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 b5627376ac8aa..0000000000000 --- 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 77215e2318ded..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/* eslint-disable react/display-name */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; -import { TestProviders } from '../../../../mock'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { ClosureType } from '../../../../../../../../plugins/case/common/api/cases'; -import * as i18n from './translations'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { getKibanaConfigError, getLicenseError } from './helpers'; -import * as api from '../../../../containers/case/configure/api'; -jest.mock('../../../../containers/case/use_get_action_license'); -jest.mock('../../../../containers/case/use_post_push_to_service'); -jest.mock('../../../../containers/case/configure/api'); - -describe('usePushToService', () => { - const caseId = '12345'; - const updateCase = jest.fn(); - const postPushToService = jest.fn(); - const mockPostPush = { - isLoading: false, - postPushToService, - }; - const closureType: ClosureType = 'close-by-user'; - const mockConnector = { - connectorId: 'c00l', - connectorName: 'name', - }; - const mockCaseConfigure = { - ...mockConnector, - createdAt: 'string', - createdBy: {}, - closureType, - updatedAt: 'string', - updatedBy: {}, - version: 'string', - }; - const getConfigureMock = jest.spyOn(api, 'getCaseConfigure'); - const actionLicense = { - id: '.servicenow', - name: 'ServiceNow', - minimumLicenseRequired: 'platinum', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }; - beforeEach(() => { - jest.resetAllMocks(); - (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense, - })); - getConfigureMock.mockImplementation(() => Promise.resolve(mockCaseConfigure)); - }); - it('push case button posts the push with correct args', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(getConfigureMock).toBeCalled(); - result.current.pushButton.props.children.props.onClick(); - expect(postPushToService).toBeCalledWith({ ...mockConnector, caseId, updateCase }); - expect(result.current.pushCallouts).toBeNull(); - }); - }); - it('Displays message when user does not have premium license', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInLicense: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getLicenseError().title); - }); - }); - it('Displays message when user does not have case enabled in config', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInConfig: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); - }); - }); - it('Displays message when user does not have a connector configured', async () => { - getConfigureMock.mockImplementation(() => - Promise.resolve({ - ...mockCaseConfigure, - connectorId: 'none', - }) - ); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); - }); - }); - it('Displays message when case is closed', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'closed', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx deleted file mode 100644 index 5092cba6872e3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useState, useMemo } from 'react'; - -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { Case } from '../../../../containers/case/types'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { getConfigureCasesUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../../home/home_navigations'; -import { CaseCallOut } from '../callout'; -import { getLicenseError, getKibanaConfigError } from './helpers'; -import * as i18n from './translations'; - -export interface UsePushToService { - caseId: string; - caseStatus: string; - isNew: boolean; - updateCase: (newCase: Case) => void; - userCanCrud: boolean; -} - -interface Connector { - connectorId: string; - connectorName: string; -} - -export interface ReturnUsePushToService { - pushButton: JSX.Element; - pushCallouts: JSX.Element | null; -} - -export const usePushToService = ({ - caseId, - caseStatus, - isNew, - updateCase, - userCanCrud, -}: UsePushToService): ReturnUsePushToService => { - const urlSearch = useGetUrlSearch(navTabs.case); - const [connector, setConnector] = useState<Connector | null>(null); - - const { isLoading, postPushToService } = usePostPushToService(); - - const handleSetConnector = useCallback((connectorId: string, connectorName?: string) => { - setConnector({ connectorId, connectorName: connectorName ?? '' }); - }, []); - - const { loading: loadingCaseConfigure } = useCaseConfigure({ - setConnector: handleSetConnector, - }); - - const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); - - const handlePushToService = useCallback(() => { - if (connector != null) { - postPushToService({ - caseId, - ...connector, - updateCase, - }); - } - }, [caseId, connector, postPushToService, updateCase]); - - const errorsMsg = useMemo(() => { - let errors: Array<{ title: string; description: JSX.Element }> = []; - if (actionLicense != null && !actionLicense.enabledInLicense) { - errors = [...errors, getLicenseError()]; - } - if ( - (connector == null || (connector != null && connector.connectorId === 'none')) && - !loadingCaseConfigure && - !loadingLicense - ) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, - description: ( - <FormattedMessage - defaultMessage="To open and update cases in external systems, you must configure a {link}." - id="xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription" - values={{ - link: ( - <EuiLink href={getConfigureCasesUrl(urlSearch)} target="_blank"> - {i18n.LINK_CONNECTOR_CONFIGURE} - </EuiLink> - ), - }} - /> - ), - }, - ]; - } - if (caseStatus === 'closed') { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, - description: ( - <FormattedMessage - defaultMessage="Closed cases cannot be sent to external systems. Reopen the case if you want to open or update it in an external system." - id="xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription" - /> - ), - }, - ]; - } - if (actionLicense != null && !actionLicense.enabledInConfig) { - errors = [...errors, getKibanaConfigError()]; - } - return errors; - }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense, urlSearch]); - - const pushToServiceButton = useMemo( - () => ( - <EuiButton - data-test-subj="push-to-service-now" - fill - iconType="importAction" - onClick={handlePushToService} - disabled={ - isLoading || - loadingLicense || - loadingCaseConfigure || - errorsMsg.length > 0 || - !userCanCrud - } - isLoading={isLoading} - > - {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW} - </EuiButton> - ), - [ - isNew, - handlePushToService, - isLoading, - loadingLicense, - loadingCaseConfigure, - errorsMsg, - userCanCrud, - ] - ); - - const objToReturn = useMemo( - () => ({ - pushButton: - errorsMsg.length > 0 ? ( - <EuiToolTip - position="top" - title={errorsMsg[0].title} - content={<p>{errorsMsg[0].description}</p>} - > - {pushToServiceButton} - </EuiToolTip> - ) : ( - <>{pushToServiceButton}</> - ), - pushCallouts: - errorsMsg.length > 0 ? ( - <CaseCallOut title={i18n.ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={errorsMsg} /> - ) : null, - }), - [errorsMsg, pushToServiceButton] - ); - return objToReturn; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx deleted file mode 100644 index d6016e540bdc0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; -import React from 'react'; - -import { CaseFullExternalService } from '../../../../../../../../plugins/case/common/api'; -import { CaseUserActions } from '../../../../containers/case/types'; -import * as i18n from '../case_view/translations'; - -interface LabelTitle { - action: CaseUserActions; - field: string; - firstIndexPushToService: number; - index: number; -} - -export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: LabelTitle) => { - if (field === 'tags') { - return getTagsLabelTitle(action); - } else if (field === 'title' && action.action === 'update') { - return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ - action.newValue - }"`; - } else if (field === 'description' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; - } else if (field === 'status' && action.action === 'update') { - return `${ - action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() - } ${i18n.CASE}`; - } else if (field === 'comment' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; - } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { - return getPushedServiceLabelTitle(action, firstIndexPushToService, index); - } - return ''; -}; - -const getTagsLabelTitle = (action: CaseUserActions) => ( - <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span"> - <EuiFlexItem data-test-subj="ua-tags-label"> - {action.action === 'add' && i18n.ADDED_FIELD} - {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - </EuiFlexItem> - {action.newValue != null && - action.newValue.split(',').map(tag => ( - <EuiFlexItem grow={false} key={tag}> - <EuiBadge data-test-subj={`ua-tag`} color="default"> - {tag} - </EuiBadge> - </EuiFlexItem> - ))} - </EuiFlexGroup> -); - -const getPushedServiceLabelTitle = ( - action: CaseUserActions, - firstIndexPushToService: number, - index: number -) => { - const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; - return ( - <EuiFlexGroup alignItems="baseline" gutterSize="xs" data-test-subj="pushed-service-label-title"> - <EuiFlexItem data-test-subj="pushed-label"> - {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiLink data-test-subj="pushed-value" href={pushedVal?.external_url} target="_blank"> - {pushedVal?.connector_name} {pushedVal?.external_title} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts deleted file mode 100644 index df9f0d08e728c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { Breadcrumb } from 'ui/chrome'; - -import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; -import { RouteSpyState } from '../../utils/route/types'; -import * as i18n from './translations'; - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { - const queryParameters = !isEmpty(search[0]) ? search[0] : null; - - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getCaseUrl(queryParameters), - }, - ]; - if (params.detailName === 'create') { - breadcrumb = [ - ...breadcrumb, - { - text: i18n.CREATE_BC_TITLE, - href: getCreateCaseUrl(queryParameters), - }, - ]; - } else if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state?.caseTitle ?? '', - href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts deleted file mode 100644 index 7e6778ca4fb4f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - getStringArray, - replaceTemplateFieldFromQuery, - replaceTemplateFieldFromMatchFilters, - reformatDataProviderWithNewValue, -} from './helpers'; -import { mockEcsData } from '../../../../mock/mock_ecs'; -import { Filter } from '../../../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; -import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; -import { cloneDeep } from 'lodash/fp'; - -describe('helpers', () => { - let mockEcsDataClone = cloneDeep(mockEcsData); - beforeEach(() => { - mockEcsDataClone = cloneDeep(mockEcsData); - }); - describe('getStringOrStringArray', () => { - test('it should correctly return a string array', () => { - const value = getStringArray('x', { - x: 'The nickname of the developer we all :heart:', - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with a single element', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:'], - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with two elements of strings', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], - }); - expect(value).toEqual([ - 'The nickname of the developer we all :heart:', - 'We are all made of stars', - ]); - }); - - test('it should correctly return a string array with deep elements', () => { - const value = getStringArray('x.y.z', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual(['zed']); - }); - - test('it should correctly return a string array with a non-existent value', () => { - const value = getStringArray('non.existent', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual([]); - }); - - test('it should trace an error if the value is not a string', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: 5 }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - 5, - 'when trying to access field:', - 'a', - 'from data object of:', - { a: 5 } - ); - }); - - test('it should trace an error if the value is an array of mixed values', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - ['hi', 5], - 'when trying to access field:', - 'a', - 'from data object of:', - { a: ['hi', 5] } - ); - }); - }); - - describe('replaceTemplateFieldFromQuery', () => { - test('given an empty query string this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('given a query string with spaces this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('it should replace a query with a template value such as apache from a mock template', () => { - const replacement = replaceTemplateFieldFromQuery( - 'host.name: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('host.name: apache'); - }); - - test('it should replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); - expect(replacement).toEqual('host.name: *'); - }); - - test('it should NOT replace a query with a template value that is not part of the template fields array', () => { - const replacement = replaceTemplateFieldFromQuery( - 'user.id: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('user.id: placeholdertext'); - }); - }); - - describe('replaceTemplateFieldFromMatchFilters', () => { - test('given an empty query filter this will return an empty filter', () => { - const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); - expect(replacement).toEqual([]); - }); - - test('given a query filter this will return that filter with the placeholder replaced', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Braden' }, - }, - query: { match_phrase: { 'host.name': 'Braden' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'apache' }, - }, - query: { match_phrase: { 'host.name': 'apache' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - - test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - }); - - describe('reformatDataProviderWithNewValue', () => { - test('it should replace a query with a template value such as apache from a mock data provider', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'user.id'; - mockDataProvider.id = 'my-id'; - mockDataProvider.name = 'Rebecca'; - mockDataProvider.queryMatch.value = 'Rebecca'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'my-id', - name: 'Rebecca', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.id', - value: 'Rebecca', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts deleted file mode 100644 index d80ef066d86ac..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isEmpty } from 'lodash/fp'; -import { Filter, esKuery, KueryNode } from '../../../../../../../../../src/plugins/data/public'; -import { - DataProvider, - DataProvidersAnd, -} from '../../../../components/timeline/data_providers/data_provider'; -import { Ecs } from '../../../../graphql/types'; - -interface FindValueToChangeInQuery { - field: string; - valueToChange: string; -} - -/** - * Fields that will be replaced with the template strings from a a saved timeline template. - * This is used for the signals detection engine feature when you save a timeline template - * and are the fields you can replace when creating a template. - */ -const templateFields = [ - 'host.name', - 'host.hostname', - 'host.domain', - 'host.id', - 'host.ip', - 'client.ip', - 'destination.ip', - 'server.ip', - 'source.ip', - 'network.community_id', - 'user.name', - 'process.name', -]; - -/** - * This will return an unknown as a string array if it exists from an unknown data type and a string - * that represents the path within the data object the same as lodash's "get". If the value is non-existent - * we will return an empty array. If it is a non string value then this will log a trace to the console - * that it encountered an error and return an empty array. - * @param field string of the field to access - * @param data The unknown data that is typically a ECS value to get the value - * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console - */ -export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { - const value: unknown | undefined = get(field, data); - if (value == null) { - return []; - } else if (typeof value === 'string') { - return [value]; - } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { - return value; - } else { - localConsole.trace( - 'Data type that is not a string or string array detected:', - value, - 'when trying to access field:', - field, - 'from data object of:', - data - ); - return []; - } -}; - -export const findValueToChangeInQuery = ( - kueryNode: KueryNode, - valueToChange: FindValueToChangeInQuery[] = [] -): FindValueToChangeInQuery[] => { - let localValueToChange = valueToChange; - if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { - localValueToChange = [ - ...localValueToChange, - { - field: kueryNode.arguments[0].value, - valueToChange: kueryNode.arguments[1].value, - }, - ]; - } - return kueryNode.arguments.reduce( - (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { - if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { - return [ - ...addValueToChange, - { - field: ast.arguments[0].value, - valueToChange: ast.arguments[1].value, - }, - ]; - } - if (ast.arguments) { - return findValueToChangeInQuery(ast, addValueToChange); - } - return addValueToChange; - }, - localValueToChange - ); -}; - -export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { - if (query.trim() !== '') { - const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); - return valueToChange.reduce((newQuery, vtc) => { - const newValue = getStringArray(vtc.field, ecsData); - if (newValue.length) { - return newQuery.replace(vtc.valueToChange, newValue[0]); - } else { - return newQuery; - } - }, query); - } else { - return ''; - } -}; - -export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => - filters.map(filter => { - if ( - filter.meta.type === 'phrase' && - filter.meta.key != null && - templateFields.includes(filter.meta.key) - ) { - const newValue = getStringArray(filter.meta.key, ecsData); - if (newValue.length) { - filter.meta.params = { query: newValue[0] }; - filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; - } - } - return filter; - }); - -export const reformatDataProviderWithNewValue = <T extends DataProvider | DataProvidersAnd>( - dataProvider: T, - ecsData: Ecs -): T => { - if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); - if (newValue.length) { - dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); - dataProvider.name = newValue[0]; - dataProvider.queryMatch.value = newValue[0]; - dataProvider.queryMatch.displayField = undefined; - dataProvider.queryMatch.displayValue = undefined; - } - } - return dataProvider; -}; - -export const replaceTemplateFieldFromDataProviders = ( - dataProviders: DataProvider[], - ecsData: Ecs -): DataProvider[] => - dataProviders.map(dataProvider => { - const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); - if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { - newDataProvider.and = newDataProvider.and.map(andDataProvider => - reformatDataProviderWithNewValue(andDataProvider, ecsData) - ); - } - return newDataProvider; - }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx deleted file mode 100644 index ce8ae2054b2c7..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { Filter, esQuery } from '../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; -import { StatefulEventsViewer } from '../../../../components/events_viewer'; -import { HeaderSection } from '../../../../components/header_section'; -import { combineQueries } from '../../../../components/timeline/helpers'; -import { useKibana } from '../../../../lib/kibana'; -import { inputsSelectors, State, inputsModel } from '../../../../store'; -import { timelineActions, timelineSelectors } from '../../../../store/timeline'; -import { TimelineModel } from '../../../../store/timeline/model'; -import { timelineDefaults } from '../../../../store/timeline/defaults'; -import { useApolloClient } from '../../../../utils/apollo_context'; - -import { updateSignalStatusAction } from './actions'; -import { - getSignalsActions, - requiredFieldsForActions, - signalsClosedFilters, - signalsDefaultModel, - signalsOpenFilters, -} from './default_config'; -import { - FILTER_CLOSED, - FILTER_OPEN, - SignalFilterOption, - SignalsTableFilterGroup, -} from './signals_filter_group'; -import { SignalsUtilityBar } from './signals_utility_bar'; -import * as i18n from './translations'; -import { - CreateTimelineProps, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateSignalsStatusCallback, - UpdateSignalsStatusProps, -} from './types'; -import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; - -export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; - -interface OwnProps { - canUserCRUD: boolean; - defaultFilters?: Filter[]; - hasIndexWrite: boolean; - from: number; - loading: boolean; - signalsIndex: string; - to: number; -} - -type SignalsTableComponentProps = OwnProps & PropsFromRedux; - -export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ - canUserCRUD, - clearEventsDeleted, - clearEventsLoading, - clearSelected, - defaultFilters, - from, - globalFilters, - globalQuery, - hasIndexWrite, - isSelectAllChecked, - loading, - loadingEventIds, - selectedEventIds, - setEventsDeleted, - setEventsLoading, - signalsIndex, - to, - updateTimeline, - updateTimelineIsLoading, -}) => { - const [selectAll, setSelectAll] = useState(false); - const apolloClient = useApolloClient(); - - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN); - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - signalsIndex !== '' ? [signalsIndex] : [] - ); - const kibana = useKibana(); - - const getGlobalQuery = useCallback(() => { - if (browserFields != null && indexPatterns != null) { - return combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders: [], - indexPattern: indexPatterns, - browserFields, - filters: isEmpty(defaultFilters) - ? globalFilters - : [...(defaultFilters ?? []), ...globalFilters], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - start: from, - end: to, - isEventViewer: true, - }); - } - return null; - }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); - - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - from: fromTimeline, - id: 'timeline-1', - notes: [], - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - - const setEventsLoadingCallback = useCallback( - ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); - }, - [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] - ); - - const setEventsDeletedCallback = useCallback( - ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); - }, - [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] - ); - - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar - useEffect(() => { - if (!isSelectAllChecked) { - setShowClearSelectionAction(false); - } else { - setSelectAll(false); - } - }, [isSelectAllChecked]); - - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: SignalFilterOption) => { - clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] - ); - - // Callback for clearing entire selection from utility bar - const clearSelectionCallback = useCallback(() => { - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setSelectAll(false); - setShowClearSelectionAction(false); - }, [clearSelected, setSelectAll, setShowClearSelectionAction]); - - // Callback for selecting all events on all pages from utility bar - // Dispatches to stateful_body's selectAll via TimelineTypeContext props - // as scope of response data required to actually set selectedEvents - const selectAllCallback = useCallback(() => { - setSelectAll(true); - setShowClearSelectionAction(true); - }, [setSelectAll, setShowClearSelectionAction]); - - const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( - async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { - await updateSignalStatusAction({ - query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, - signalIds: Object.keys(selectedEventIds), - status, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - }); - refetchQuery(); - }, - [ - getGlobalQuery, - selectedEventIds, - setEventsDeletedCallback, - setEventsLoadingCallback, - showClearSelectionAction, - ] - ); - - // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component - const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => { - return ( - <SignalsUtilityBar - canUserCRUD={canUserCRUD} - areEventsLoading={loadingEventIds.length > 0} - clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - isFilteredToOpen={filterGroup === FILTER_OPEN} - selectAll={selectAllCallback} - selectedEventIds={selectedEventIds} - showClearSelection={showClearSelectionAction} - totalCount={totalCount} - updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} - /> - ); - }, - [ - canUserCRUD, - hasIndexWrite, - clearSelectionCallback, - filterGroup, - loadingEventIds.length, - selectAllCallback, - selectedEventIds, - showClearSelectionAction, - updateSignalsStatusCallback, - ] - ); - - // Send to Timeline / Update Signal Status Actions for each table row - const additionalActions = useMemo( - () => - getSignalsActions({ - apolloClient, - canUserCRUD, - hasIndexWrite, - createTimeline: createTimelineCallback, - setEventsLoading: setEventsLoadingCallback, - setEventsDeleted: setEventsDeletedCallback, - status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, - updateTimelineIsLoading, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - updateTimelineIsLoading, - ] - ); - - const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); - const defaultFiltersMemo = useMemo(() => { - if (isEmpty(defaultFilters)) { - return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; - } else if (defaultFilters != null && !isEmpty(defaultFilters)) { - return [ - ...defaultFilters, - ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), - ]; - } - }, [defaultFilters, filterGroup]); - - const timelineTypeContext = useMemo( - () => ({ - documentType: i18n.SIGNALS_DOCUMENT_TYPE, - footerText: i18n.TOTAL_COUNT_OF_SIGNALS, - loadingText: i18n.LOADING_SIGNALS, - queryFields: requiredFieldsForActions, - timelineActions: additionalActions, - title: i18n.SIGNALS_TABLE_TITLE, - selectAll: canUserCRUD ? selectAll : false, - }), - [additionalActions, canUserCRUD, selectAll] - ); - - const headerFilterGroup = useMemo( - () => <SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />, - [onFilterGroupChangedCallback] - ); - - if (loading || isEmpty(signalsIndex)) { - return ( - <EuiPanel> - <HeaderSection title={i18n.SIGNALS_TABLE_TITLE} /> - <EuiLoadingContent data-test-subj="loading-signals-panel" /> - </EuiPanel> - ); - } - - return ( - <StatefulEventsViewer - defaultIndices={defaultIndices} - pageFilters={defaultFiltersMemo} - defaultModel={signalsDefaultModel} - end={to} - headerFilterGroup={headerFilterGroup} - id={SIGNALS_PAGE_TIMELINE_ID} - start={from} - timelineTypeContext={timelineTypeContext} - utilityBar={utilityBarCallback} - /> - ); -}; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getGlobalInputs = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State) => { - const timeline: TimelineModel = - getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; - const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; - - const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - return { - globalQuery: query, - globalFilters: filters, - deletedEventIds, - isSelectAllChecked, - loadingEventIds, - selectedEventIds, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), - setEventsLoading: ({ - id, - eventIds, - isLoading, - }: { - id: string; - eventIds: string[]; - isLoading: boolean; - }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsLoading({ id })), - setEventsDeleted: ({ - id, - eventIds, - isDeleted, - }: { - id: string; - eventIds: string[]; - isDeleted: boolean; - }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx deleted file mode 100644 index 847fcc7860085..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../../plugins/siem/common/constants'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../../components/utility_bar'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; - -interface SignalsUtilityBarProps { - canUserCRUD: boolean; - hasIndexWrite: boolean; - areEventsLoading: boolean; - clearSelection: () => void; - isFilteredToOpen: boolean; - selectAll: () => void; - selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - showClearSelection: boolean; - totalCount: number; - updateSignalsStatus: UpdateSignalsStatus; -} - -const SignalsUtilityBarComponent: React.FC<SignalsUtilityBarProps> = ({ - canUserCRUD, - hasIndexWrite, - areEventsLoading, - clearSelection, - totalCount, - selectedEventIds, - isFilteredToOpen, - selectAll, - showClearSelection, - updateSignalsStatus, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - - const handleUpdateStatus = useCallback(async () => { - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), - status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, - }); - }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); - - const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); - const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( - defaultNumberFormat - ); - - return ( - <> - <UtilityBar> - <UtilityBarSection> - <UtilityBarGroup> - <UtilityBarText dataTestSubj="showingSignals"> - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - </UtilityBarText> - </UtilityBarGroup> - - <UtilityBarGroup> - {canUserCRUD && hasIndexWrite && ( - <> - <UtilityBarText dataTestSubj="selectedSignals"> - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - </UtilityBarText> - - <UtilityBarAction - dataTestSubj="openCloseSignal" - disabled={areEventsLoading || isEmpty(selectedEventIds)} - iconType={isFilteredToOpen ? 'securitySignalResolved' : 'securitySignalDetected'} - onClick={handleUpdateStatus} - > - {isFilteredToOpen - ? i18n.BATCH_ACTION_CLOSE_SELECTED - : i18n.BATCH_ACTION_OPEN_SELECTED} - </UtilityBarAction> - - <UtilityBarAction - iconType={showClearSelection ? 'cross' : 'pagesSelect'} - onClick={() => { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - </UtilityBarAction> - </> - )} - </UtilityBarGroup> - </UtilityBarSection> - </UtilityBar> - </> - ); -}; - -export const SignalsUtilityBar = React.memo( - SignalsUtilityBarComponent, - (prevProps, nextProps) => - prevProps.areEventsLoading === nextProps.areEventsLoading && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection -); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx deleted file mode 100644 index 90bdd39e4a6fa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { showAllOthersBucket } from '../../../../../../../../plugins/siem/common/constants'; -import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; -import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; -import * as i18n from './translations'; - -export const formatSignalsData = ( - signalsData: SignalSearchResponse<{}, SignalsAggregation> | null -) => { - const groupBuckets: SignalsGroupBucket[] = - signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; - return groupBuckets.reduce<HistogramData[]>((acc, { key: group, signals }) => { - const signalsBucket: SignalsBucket[] = signals.buckets ?? []; - - return [ - ...acc, - ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }, []); -}; - -export const getSignalsHistogramQuery = ( - stackByField: string, - from: number, - to: number, - additionalFilters: Array<{ - bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; - }> -) => { - const missing = showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - } - : {}; - - return { - aggs: { - signalsByGrouping: { - terms: { - field: stackByField, - ...missing, - order: { - _count: 'desc', - }, - size: 10, - }, - aggs: { - signals: { - date_histogram: { - field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, - min_doc_count: 0, - extended_bounds: { - min: from, - max: to, - }, - }, - }, - }, - }, - }, - query: { - bool: { - filter: [ - ...additionalFilters, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - ], - }, - }, - }; -}; - -/** - * Returns `true` when the signals histogram initial loading spinner should be shown - * - * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed - * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) - */ -export const showInitialLoadingSpinner = ({ - isInitialLoading, - isLoadingSignals, -}: { - isInitialLoading: boolean; - isLoadingSignals: boolean; -}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx deleted file mode 100644 index e70ba804ec018..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Position } from '@elastic/charts'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { LegendItem } from '../../../../components/charts/draggable_legend_item'; -import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; -import { HeaderSection } from '../../../../components/header_section'; -import { Filter, esQuery, Query } from '../../../../../../../../../src/plugins/data/public'; -import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; -import { getDetectionEngineUrl } from '../../../../components/link_to'; -import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; -import { InspectButtonContainer } from '../../../../components/inspect'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; -import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; -import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { navTabs } from '../../../home/home_navigations'; -import { signalsHistogramOptions } from './config'; -import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; -import { SignalsHistogram } from './signals_histogram'; -import * as i18n from './translations'; -import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; - -const DEFAULT_PANEL_HEIGHT = 300; - -const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} - position: relative; -`; - -const defaultTotalSignalsObj: SignalsTotal = { - value: 0, - relation: 'eq', -}; - -export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; - -const ViewSignalsFlexItem = styled(EuiFlexItem)` - margin-left: 24px; -`; - -interface SignalsHistogramPanelProps { - chartHeight?: number; - defaultStackByOption?: SignalsHistogramOption; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - legendPosition?: Position; - panelHeight?: number; - signalIndexName: string | null; - setQuery: (params: RegisterQuery) => void; - showLinkToSignals?: boolean; - showTotalSignalsCount?: boolean; - stackByOptions?: SignalsHistogramOption[]; - title?: string; - to: number; - updateDateRange: (min: number, max: number) => void; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const NO_LEGEND_DATA: LegendItem[] = []; - -export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( - ({ - chartHeight, - defaultStackByOption = signalsHistogramOptions[0], - deleteQuery, - filters, - headerChildren, - onlyField, - query, - from, - legendPosition = 'right', - panelHeight = DEFAULT_PANEL_HEIGHT, - setQuery, - signalIndexName, - showLinkToSignals = false, - showTotalSignalsCount = false, - stackByOptions, - to, - title = i18n.HISTOGRAM_HEADER, - updateDateRange, - }) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); - const [isInitialLoading, setIsInitialLoading] = useState(true); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>( - onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) - ); - const { - loading: isLoadingSignals, - data: signalsData, - setQuery: setSignalsQuery, - response, - request, - refetch, - } = useQuerySignals<{}, SignalsAggregation>( - getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), - signalIndexName - ); - const kibana = useKibana(); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const totalSignals = useMemo( - () => - i18n.SHOWING_SIGNALS( - numeral(totalSignalsObj.value).format(defaultNumberFormat), - totalSignalsObj.value, - totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' - ), - [totalSignalsObj] - ); - - const setSelectedOptionCallback = useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { - setSelectedStackByOption( - stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption - ); - }, []); - - const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); - - const legendItems: LegendItem[] = useMemo( - () => - signalsData?.aggregations?.signalsByGrouping?.buckets != null - ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ - color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, - dataProviderId: escapeDataProviderId( - `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` - ), - field: selectedStackByOption.value, - value: bucket.key, - })) - : NO_LEGEND_DATA, - [signalsData, selectedStackByOption.value] - ); - - useEffect(() => { - let canceled = false; - - if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { - setIsInitialLoading(false); - } - - return () => { - canceled = true; // prevent long running data fetches from updating state after unmounting - }; - }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, []); - - useEffect(() => { - if (refetch != null && setQuery != null) { - setQuery({ - id: uniqueQueryId, - inspect: { - dsl: [request], - response: [response], - }, - loading: isLoadingSignals, - refetch, - }); - } - }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); - - useEffect(() => { - setTotalSignalsObj( - signalsData?.hits.total ?? { - value: 0, - relation: 'eq', - } - ); - }, [signalsData]); - - useEffect(() => { - const converted = esQuery.buildEsQuery( - undefined, - query != null ? [query] : [], - filters?.filter(f => f.meta.disabled === false) ?? [], - { - ...esQuery.getEsQueryConfig(kibana.services.uiSettings), - dateFormatTZ: undefined, - } - ); - - setSignalsQuery( - getSignalsHistogramQuery( - selectedStackByOption.value, - from, - to, - !isEmpty(converted) ? [converted] : [] - ) - ); - }, [selectedStackByOption.value, from, to, query, filters]); - - const linkButton = useMemo(() => { - if (showLinkToSignals) { - return ( - <ViewSignalsFlexItem grow={false}> - <EuiButton href={getDetectionEngineUrl(urlSearch)}>{i18n.VIEW_SIGNALS}</EuiButton> - </ViewSignalsFlexItem> - ); - } - }, [showLinkToSignals, urlSearch]); - - const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ - onlyField, - title, - ]); - - return ( - <InspectButtonContainer data-test-subj="signals-histogram-panel" show={!isInitialLoading}> - <StyledEuiPanel height={panelHeight}> - <HeaderSection - id={uniqueQueryId} - title={titleText} - titleSize={onlyField == null ? 'm' : 's'} - subtitle={!isInitialLoading && showTotalSignalsCount && totalSignals} - > - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem grow={false}> - {stackByOptions && ( - <EuiSelect - onChange={setSelectedOptionCallback} - options={stackByOptions} - prepend={i18n.STACK_BY_LABEL} - value={selectedStackByOption.value} - /> - )} - {headerChildren != null && headerChildren} - </EuiFlexItem> - {linkButton} - </EuiFlexGroup> - </HeaderSection> - - {isInitialLoading ? ( - <MatrixLoader /> - ) : ( - <SignalsHistogram - chartHeight={chartHeight} - data={formattedSignalsData} - from={from} - legendItems={legendItems} - legendPosition={legendPosition} - loading={isLoadingSignals} - to={to} - updateDateRange={updateDateRange} - /> - )} - </StyledEuiPanel> - </InspectButtonContainer> - ); - } -); - -SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts deleted file mode 100644 index 5e0293325289b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; -import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; -import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; -import { FieldValueQueryBar } from '../../components/query_bar'; - -export const mockQueryBar: FieldValueQueryBar = { - query: { - query: 'test query', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -export const mockRule = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Home Grown!', - query: '', - references: [], - saved_id: "Garrett's IP", - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'saved_query', - threat: [], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockRuleWithEverything = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: ['test'], - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Query with rule-id', - query: 'user.name: root or user.name: admin', - references: ['www.test.co'], - saved_id: 'test123', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: ['tag1', 'tag2'], - to: 'now', - type: 'saved_query', - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ - isNew, - name: 'Query with rule-id', - description: '24/7', - severity: 'low', - riskScore: 21, - references: ['www.test.co'], - falsePositives: ['test'], - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - note: '# this is some markdown documentation', -}); - -export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ - isNew, - actions: [], - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', - enabled, - throttle: 'no_actions', -}); - -export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ - isNew, - ruleType: 'query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['filebeat-'], - queryBar: mockQueryBar, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, -}); - -export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ - isNew, - interval: '5m', - from: '6m', - to: 'now', -}); - -export const mockRuleError = (id: string): RuleError => ({ - rule_id: id, - error: { status_code: 404, message: `id: "${id}" not found` }, -}); - -export const mockRules: Rule[] = [ - mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), - mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx deleted file mode 100644 index af946c6f02cbb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { EuiLoadingSpinner } from '@elastic/eui'; - -import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; -import { esFilters, FilterManager } from '../../../../../../../../../../src/plugins/data/public'; -import { SeverityBadge } from '../severity_badge'; - -import * as i18n from './translations'; -import { - isNotEmptyArray, - buildQueryBarDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildStringArrayDescription, - buildSeverityDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { ListItems } from './types'; - -const setupMock = coreMock.createSetup(); -const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } -}; -setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); -const mockFilterManager = new FilterManager(setupMock.uiSettings); - -const mockQueryBar = { - query: 'test query', - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -describe('helpers', () => { - describe('isNotEmptyArray', () => { - test('returns false if empty array', () => { - const result = isNotEmptyArray([]); - expect(result).toBeFalsy(); - }); - - test('returns false if array of empty strings', () => { - const result = isNotEmptyArray(['', '']); - expect(result).toBeFalsy(); - }); - - test('returns true if array of string with space', () => { - const result = isNotEmptyArray([' ']); - expect(result).toBeTruthy(); - }); - - test('returns true if array with at least one non-empty string', () => { - const result = isNotEmptyArray(['', 'abc']); - expect(result).toBeTruthy(); - }); - }); - - describe('buildQueryBarDescription', () => { - test('returns empty array if no filters, query or savedId exist', () => { - const emptyMockQueryBar = { - query: '', - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: emptyMockQueryBar.filters, - filterManager: mockFilterManager, - query: emptyMockQueryBar.query, - savedId: emptyMockQueryBar.saved_id, - }); - expect(result).toEqual([]); - }); - - test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); - expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); - }); - - test('returns expected array of ListItems when filters AND indexPatterns exist', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); - expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); - expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); - }); - - test('returns expected array of ListItems when "query.query" exists', () => { - const mockQueryBarWithQuery = { - ...mockQueryBar, - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithQuery.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithQuery.query, - savedId: mockQueryBarWithQuery.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} </>); - }); - - test('returns expected array of ListItems when "savedId" exists', () => { - const mockQueryBarWithSavedId = { - ...mockQueryBar, - query: '', - filters: [], - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithSavedId.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithSavedId.query, - savedId: mockQueryBarWithSavedId.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} </>); - }); - }); - - describe('buildThreatDescription', () => { - test('returns empty array if no threats', () => { - const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); - expect(result).toHaveLength(0); - }); - - test('returns empty tactic link if no corresponding tactic id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns empty technique link if no corresponding technique id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); - }); - - test('returns with corresponding tactic and technique link text', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns corresponding number of tactic and technique links', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, - { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, - ], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, - ], - tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); - }); - }); - - describe('buildUnorderedListArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - [] - ); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - ['', 'falsePositive1', 'falsePositive2'] - ); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); - }); - }); - - describe('buildStringArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ - '', - 'tag1', - 'tag2', - ]); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .first() - .text() - ).toEqual('tag1'); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .at(1) - .text() - ).toEqual('tag2'); - }); - }); - - describe('buildSeverityDescription', () => { - test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); - - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(<SeverityBadge value="Test description value" />); - }); - }); - - describe('buildUrlsDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUrlsDescription('Test label', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUrlsDescription('Test label', [ - 'www.test.com', - 'www.test2.com', - ]); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .first() - .text() - ).toEqual('www.test.com'); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .at(1) - .text() - ).toEqual('www.test2.com'); - }); - }); - - describe('buildNoteDescription', () => { - test('returns ListItem with passed in label and note content', () => { - const noteSample = - 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; - const result: ListItems[] = buildNoteDescription('Test label', noteSample); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); - - expect(result[0].title).toEqual('Test label'); - expect(noteElement.exists()).toBeTruthy(); - expect(noteElement.text()).toEqual(noteSample); - }); - - test('returns empty array if passed in note is empty string', () => { - const result: ListItems[] = buildNoteDescription('Test label', ''); - - expect(result).toHaveLength(0); - }); - }); - - describe('buildRuleTypeDescription', () => { - it('returns the label for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.description).toEqual('Machine Learning'); - }); - - it('returns the label for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.description).toEqual('Query'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx deleted file mode 100644 index d3014354ab7b3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiBadge, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; - -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; - -import * as i18n from './translations'; -import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; -import { SeverityBadge } from '../severity_badge'; -import ListTreeIcon from './assets/list_tree_icon.svg'; -import { assertUnreachable } from '../../../../../lib/helpers'; - -const NoteDescriptionContainer = styled(EuiFlexItem)` - height: 105px; - overflow-y: hidden; -`; - -export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); - -const EuiBadgeWrap = styled(EuiBadge)` - .euiBadge__text { - white-space: pre-wrap !important; - } -` 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 8e8927cb7bbd1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { - StepRuleDescriptionComponent, - addFilterStateIfNotThere, - buildListItems, - getDescriptionItem, -} from './'; - -import { - esFilters, - Filter, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { mockAboutStepRule, mockDefineStepRule } from '../../all/__mocks__/mock'; -import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import * as i18n from './translations'; - -import { schema } from '../step_about_rule/schema'; -import { ListItems } from './types'; -import { AboutStepRule } from '../../types'; - -jest.mock('../../../../../lib/kibana'); - -describe('description_step', () => { - const setupMock = coreMock.createSetup(); - const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } - }; - let mockFilterManager: FilterManager; - let mockAboutStep: AboutStepRule; - - beforeEach(() => { - setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); - mockFilterManager = new FilterManager(setupMock.uiSettings); - mockAboutStep = mockAboutStepRule(); - }); - - describe('StepRuleDescriptionComponent', () => { - test('renders correctly against snapshot when columns is "multi"', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); - }); - - test('renders correctly against snapshot when columns is "single"', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - }); - - test('renders correctly against snapshot when columns is "singleSplit', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - expect( - wrapper - .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') - .at(0) - .prop('type') - ).toEqual('column'); - }); - }); - - describe('addFilterStateIfNotThere', () => { - test('it does not change the state if it is global', () => { - const filters: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - - test('it adds the state if it does not exist as local', () => { - const filters: Filter[] = [ - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - }); - - describe('buildListItems', () => { - test('returns expected ListItems array when given valid inputs', () => { - const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); - - expect(result.length).toEqual(9); - }); - }); - - describe('getDescriptionItem', () => { - test('returns ListItem with all values enumerated when value[field] is an array', () => { - const result: ListItems[] = getDescriptionItem( - 'tags', - 'Tags label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Tags label'); - expect(typeof result[0].description).toEqual('object'); - }); - - test('returns ListItem with description of value[field] when value[field] is a string', () => { - const result: ListItems[] = getDescriptionItem( - 'description', - 'Description label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Description label'); - expect(result[0].description).toEqual('24/7'); - }); - - test('returns empty array when "value" is a non-existant property in "field"', () => { - const result: ListItems[] = getDescriptionItem( - 'jibberjabber', - 'JibberJabber label', - mockAboutStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - - describe('queryBar', () => { - test('returns array of ListItems when queryBar exist', () => { - const mockQueryBar = { - isNew: false, - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: null, - saved_id: null, - }, - }; - const result: ListItems[] = getDescriptionItem( - 'queryBar', - 'Query bar label', - mockQueryBar, - mockFilterManager - ); - - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} </>); - }); - }); - - describe('threat', () => { - test('returns array of ListItems when threat exist', () => { - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Threat label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - - test('filters out threats with tactic.name of "none"', () => { - const mockStep = { - ...mockAboutStep, - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'none', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - }); - - describe('references', () => { - test('returns array of ListItems when references exist', () => { - const result: ListItems[] = getDescriptionItem( - 'references', - 'Reference label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Reference label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('falsePositives', () => { - test('returns array of ListItems when falsePositives exist', () => { - const result: ListItems[] = getDescriptionItem( - 'falsePositives', - 'False positives label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('False positives label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('severity', () => { - test('returns array of ListItems when severity exist', () => { - const result: ListItems[] = getDescriptionItem( - 'severity', - 'Severity label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Severity label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('riskScore', () => { - test('returns array of ListItems when riskScore exist', () => { - const result: ListItems[] = getDescriptionItem( - 'riskScore', - 'Risk score label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Risk score label'); - expect(result[0].description).toEqual(21); - }); - }); - - describe('timeline', () => { - test('returns timeline title if one exists', () => { - const mockDefineStep = mockDefineStepRule(); - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockDefineStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual('Titled timeline'); - }); - - test('returns default timeline title if none exists', () => { - const mockStep = { - ...mockDefineStepRule(), - timeline: { - id: '12345', - }, - }; - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); - }); - }); - - describe('note', () => { - test('returns default "note" description', () => { - const result: ListItems[] = getDescriptionItem( - 'note', - 'Investigation guide', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Investigation guide'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx deleted file mode 100644 index 49977713a585a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; -import React, { memo, useState } from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { - IIndexPattern, - Filter, - esFilters, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useKibana } from '../../../../../lib/kibana'; -import { IMitreEnterpriseAttack } from '../../types'; -import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../../../../../shared_imports'; -import { ListItems } from './types'; -import { - buildQueryBarDescription, - buildSeverityDescription, - buildStringArrayDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { buildMlJobDescription } from './ml_job_description'; - -const DescriptionListContainer = styled(EuiDescriptionList)` - &.euiDescriptionList--column .euiDescriptionList__title { - width: 30%; - } - &.euiDescriptionList--column .euiDescriptionList__description { - width: 70%; - } -`; - -interface StepRuleDescriptionProps { - columns?: 'multi' | 'single' | 'singleSplit'; - data: unknown; - indexPatterns?: IIndexPattern; - schema: FormSchema; -} - -export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({ - data, - columns = 'multi', - indexPatterns, - schema, -}) => { - const kibana = useKibana(); - const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); - - const keys = Object.keys(schema); - const listItems = keys.reduce((acc: ListItems[], key: string) => { - if (key === 'machineLearningJobId') { - return [ - ...acc, - buildMlJobDescription( - get(key, data) as string, - (get(key, schema) as { label: string }).label, - siemJobs - ), - ]; - } - return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; - }, []); - - if (columns === 'multi') { - return ( - <EuiFlexGroup> - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - <EuiFlexItem - data-test-subj="listItemColumnStepRuleDescription" - key={`description-step-rule-${index}`} - > - <EuiDescriptionList listItems={chunkListItems} /> - </EuiFlexItem> - ))} - </EuiFlexGroup> - ); - } - - return ( - <EuiFlexGroup> - <EuiFlexItem data-test-subj="listItemColumnStepRuleDescription"> - {columns === 'single' ? ( - <EuiDescriptionList listItems={listItems} /> - ) : ( - <DescriptionListContainer - data-test-subj="singleSplitStepRuleDescriptionList" - type="column" - listItems={listItems} - /> - )} - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -export const StepRuleDescription = memo(StepRuleDescriptionComponent); - -export const buildListItems = ( - data: unknown, - schema: FormSchema, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => - Object.keys(schema).reduce<ListItems[]>( - (acc, field) => [ - ...acc, - ...getDescriptionItem( - field, - get([field, 'label'], schema), - data, - filterManager, - indexPatterns - ), - ], - [] - ); - -export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { - return filters.map(filter => { - if (filter.$state == null) { - return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; - } else { - return filter; - } - }); -}; - -export const getDescriptionItem = ( - field: string, - label: string, - data: unknown, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => { - if (field === 'queryBar') { - const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); - const query = get('queryBar.query.query', data); - const savedId = get('queryBar.saved_id', data); - return buildQueryBarDescription({ - field, - filters, - filterManager, - query, - savedId, - indexPatterns, - }); - } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, data).filter( - (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' - ); - return buildThreatDescription({ label, threat }); - } else if (field === 'references') { - const urls: string[] = get(field, data); - return buildUrlsDescription(label, urls); - } else if (field === 'falsePositives') { - const values: string[] = get(field, data); - return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, data))) { - const values: string[] = get(field, data); - return buildStringArrayDescription(label, field, values); - } else if (field === 'severity') { - const val: string = get(field, data); - return buildSeverityDescription(label, val); - } else if (field === 'timeline') { - const timeline = get(field, data) as FieldValueTimeline; - return [ - { - title: label, - description: timeline.title ?? DEFAULT_TIMELINE_TITLE, - }, - ]; - } else if (field === 'note') { - const val: string = get(field, data); - return buildNoteDescription(label, val); - } else if (field === 'ruleType') { - const ruleType: RuleType = get(field, data); - return buildRuleTypeDescription(label, ruleType); - } - - const description: string = get(field, data); - if (isNumber(description) || !isEmpty(description)) { - return [ - { - title: label, - description, - }, - ]; - } - return []; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts deleted file mode 100644 index bfca6b2068443..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { ReactNode } from 'react'; - -import { - IIndexPattern, - Filter, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { IMitreEnterpriseAttack } from '../../types'; - -export interface ListItems { - title: NonNullable<ReactNode>; - description: NonNullable<ReactNode>; -} - -export interface BuildQueryBarDescription { - field: string; - filters: Filter[]; - filterManager: FilterManager; - query: string; - savedId: string; - indexPatterns?: IIndexPattern; -} - -export interface BuildThreatDescription { - label: string; - threat: IMitreEnterpriseAttack[]; -} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx deleted file mode 100644 index 82350150488d0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; - -import styled from 'styled-components'; -import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { useKibana } from '../../../../../lib/kibana'; -import { - ML_JOB_SELECT_PLACEHOLDER_TEXT, - ENABLE_ML_JOB_WARNING, -} from '../step_define_rule/translations'; - -const HelpTextWarningContainer = styled.div` - margin-top: 10px; -`; - -const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 5px; -`; - -const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ - href, - showEnableWarning = false, -}) => ( - <> - <FormattedMessage - id="xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText" - defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of “siem” to those jobs in the {machineLearning} application to make them appear here." - values={{ - machineLearning: ( - <EuiLink href={href} target="_blank"> - <FormattedMessage - id="xpack.siem.components.mlJobSelect.machineLearningLink" - defaultMessage="Machine Learning" - /> - </EuiLink> - ), - }} - /> - {showEnableWarning && ( - <HelpTextWarningContainer> - <EuiText size="xs" color="warning"> - <EuiIcon type="alert" /> - <span>{ENABLE_ML_JOB_WARNING}</span> - </EuiText> - </HelpTextWarningContainer> - )} - </> -); - -const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( - <> - <strong>{title}</strong> - <EuiText size="xs" color="subdued"> - <p>{description}</p> - </EuiText> - </> -); - -interface MlJobSelectProps { - describedByIds: string[]; - field: FieldHook; -} - -export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => { - const jobId = field.value as string; - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); - const mlUrl = useKibana().services.application.getUrlForApp('ml'); - const handleJobChange = useCallback( - (machineLearningJobId: string) => { - field.setValue(machineLearningJobId); - }, - [field] - ); - const placeholderOption = { - value: 'placeholder', - inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - disabled: true, - }; - - const jobOptions = siemJobs.map(job => ({ - value: job.id, - inputDisplay: job.id, - dropdownDisplay: <JobDisplay title={job.id} description={job.description} />, - })); - - const options = [placeholderOption, ...jobOptions]; - - const isJobRunning = useMemo(() => { - // If the selected job is not found in the list, it means the placeholder is selected - // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find(j => j.id === jobId); - return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); - - return ( - <MlJobSelectEuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label={field.label} - helpText={<HelpText href={mlUrl} showEnableWarning={!isJobRunning} />} - isInvalid={isInvalid} - error={errorMessage} - data-test-subj="mlJobSelect" - describedByIds={describedByIds} - > - <EuiFlexGroup> - <EuiFlexItem> - <EuiSuperSelect - hasDividers - isLoading={isLoading} - onChange={handleJobChange} - options={options} - valueOfSelected={jobId || 'placeholder'} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - </EuiFlexItem> - </MlJobSelectEuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx deleted file mode 100644 index d232c86c19e6f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Subscription } from 'rxjs'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - Query, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../../../containers/source'; -import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; -import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; -import { QueryBar } from '../../../../../components/query_bar'; -import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; -import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; -import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; -import { useKibana } from '../../../../../lib/kibana'; -import { TimelineModel } from '../../../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import * as i18n from './translations'; - -export interface FieldValueQueryBar { - filters: Filter[]; - query: Query; - saved_id?: string; -} -interface QueryBarDefineRuleProps { - browserFields: BrowserFields; - dataTestSubj: string; - field: FieldHook; - idAria: string; - isLoading: boolean; - indexPattern: IIndexPattern; - onCloseTimelineSearch: () => void; - openTimelineSearch: boolean; - resizeParentContainer?: (height: number) => void; -} - -const StyledEuiFormRow = styled(EuiFormRow)` - .kbnTypeahead__items { - max-height: 45vh !important; - } - .globalQueryBar { - padding: 4px 0px 0px 0px; - .kbnQueryBar { - & > div:first-child { - margin: 0px 0px 0px 4px; - } - } - } -`; - -// TODO need to add disabled in the SearchBar - -export const QueryBarDefineRule = ({ - browserFields, - dataTestSubj, - field, - idAria, - indexPattern, - isLoading = false, - onCloseTimelineSearch, - openTimelineSearch = false, - resizeParentContainer, -}: QueryBarDefineRuleProps) => { - const [originalHeight, setOriginalHeight] = useState(-1); - const [loadingTimeline, setLoadingTimeline] = useState(false); - const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); - const [queryDraft, setQueryDraft] = useState<Query>({ query: '', language: 'kuery' }); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const kibana = useKibana(); - const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); - - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters([]); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const newFilters = filterManager.getFilters(); - const { filters } = field.value as FieldValueQueryBar; - - if (!deepEqual(filters, newFilters)) { - field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); - } - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, [field.value]); - - useEffect(() => { - let isSubscribed = true; - async function updateFilterQueryFromValue() { - const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!deepEqual(query, queryDraft)) { - setQueryDraft(query); - } - if (!deepEqual(filters, filterManager.getFilters())) { - filterManager.setFilters(filters); - } - if ( - (savedId != null && savedQuery != null && savedId !== savedQuery.id) || - (savedId != null && savedQuery == null) - ) { - try { - const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery(mySavedQuery); - } - } catch { - setSavedQuery(null); - } - } else if (savedId == null && savedQuery != null) { - setSavedQuery(null); - } - } - updateFilterQueryFromValue(); - return () => { - isSubscribed = false; - }; - }, [field.value]); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - const { saved_id: savedId } = field.value as FieldValueQueryBar; - if (newSavedQuery.id !== savedId) { - setSavedQuery(newSavedQuery); - field.setValue({ - filters: newSavedQuery.attributes.filters, - query: newSavedQuery.attributes.query, - saved_id: newSavedQuery.id, - }); - } - } - }, - [field.value] - ); - - const onCloseTimelineModal = useCallback(() => { - setLoadingTimeline(true); - onCloseTimelineSearch(); - }, [onCloseTimelineSearch]); - - const onOpenTimeline = useCallback( - (timeline: TimelineModel) => { - setLoadingTimeline(false); - const newQuery = { - query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', - language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', - }; - const dataProvidersDsl = - timeline.dataProviders != null && timeline.dataProviders.length > 0 - ? convertKueryToElasticSearchQuery( - buildGlobalQuery(timeline.dataProviders, browserFields), - indexPattern - ) - : ''; - const newFilters = timeline.filters ?? []; - field.setValue({ - filters: - dataProvidersDsl !== '' - ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] - : newFilters, - query: newQuery, - saved_id: '', - }); - }, - [browserFields, field, indexPattern] - ); - - const onMutation = (event: unknown, observer: unknown) => { - if (resizeParentContainer != null) { - const suggestionContainer = document.getElementById('kbnTypeahead__items'); - if (suggestionContainer != null) { - const box = suggestionContainer.getBoundingClientRect(); - const accordionContainer = document.getElementById('define-rule'); - if (accordionContainer != null) { - const accordionBox = accordionContainer.getBoundingClientRect(); - if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { - resizeParentContainer(originalHeight + box.height - 100); - } - if (originalHeight === -1) { - setOriginalHeight(accordionBox.height); - } - } - } else { - resizeParentContainer(-1); - } - } - }; - - const actionTimelineToHide = useMemo<ActionTimelineToShow[]>(() => ['duplicate'], []); - - return ( - <> - <StyledEuiFormRow - label={field.label} - labelAppend={field.labelAppend} - helpText={field.helpText} - error={errorMessage} - isInvalid={isInvalid} - fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} - > - <EuiMutationObserver - observerOptions={{ subtree: true, attributes: true, childList: true }} - onMutation={onMutation} - > - {mutationRef => ( - <div ref={mutationRef}> - <QueryBar - indexPattern={indexPattern} - isLoading={isLoading || loadingTimeline} - isRefreshPaused={false} - filterQuery={queryDraft} - filterManager={filterManager} - filters={filterManager.getFilters() || []} - onChangedQuery={onChangedQuery} - onSubmitQuery={onSubmitQuery} - savedQuery={savedQuery} - onSavedQuery={onSavedQuery} - hideSavedQuery={false} - /> - </div> - )} - </EuiMutationObserver> - </StyledEuiFormRow> - {openTimelineSearch ? ( - <OpenTimelineModal - hideActions={actionTimelineToHide} - modalTitle={i18n.IMPORT_TIMELINE_MODAL} - onClose={onCloseTimelineModal} - onOpen={onOpenTimeline} - /> - ) : null} - </> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx deleted file mode 100644 index b4d813c48b43f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useEffect, useState } from 'react'; -import deepMerge from 'deepmerge'; - -import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../../../../plugins/siem/common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loadActionTypes } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/lib/action_connector_api'; -import { SelectField } from '../../../../../shared_imports'; -import { - ActionForm, - ActionType, -} from '../../../../../../../../../plugins/triggers_actions_ui/public'; -import { AlertAction } from '../../../../../../../../../plugins/alerting/common'; -import { useKibana } from '../../../../../lib/kibana'; - -type ThrottleSelectField = typeof SelectField; - -const DEFAULT_ACTION_GROUP_ID = 'default'; -const DEFAULT_ACTION_MESSAGE = - 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; - -export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { - const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); - const { - http, - triggers_actions_ui: { actionTypeRegistry }, - notifications, - } = useKibana().services; - - const setActionIdByIndex = useCallback( - (id: string, index: number) => { - const updatedActions = [...(field.value as Array<Partial<AlertAction>>)]; - updatedActions[index] = deepMerge(updatedActions[index], { id }); - field.setValue(updatedActions); - }, - [field] - ); - - const setAlertProperty = useCallback( - (updatedActions: AlertAction[]) => field.setValue(updatedActions), - [field] - ); - - const setActionParamsProperty = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (key: string, value: any, index: number) => { - const updatedActions = [...(field.value as AlertAction[])]; - updatedActions[index].params[key] = value; - field.setValue(updatedActions); - }, - [field] - ); - - useEffect(() => { - (async function() { - const actionTypes = await loadActionTypes({ http }); - const supportedTypes = actionTypes.filter(actionType => - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) - ); - setSupportedActionTypes(supportedTypes); - })(); - }, []); - - if (!supportedActionTypes) return <></>; - - return ( - <ActionForm - actions={field.value as AlertAction[]} - messageVariables={messageVariables} - defaultActionGroupId={DEFAULT_ACTION_GROUP_ID} - setActionIdByIndex={setActionIdByIndex} - setAlertProperty={setAlertProperty} - setActionParamsProperty={setActionParamsProperty} - http={http} - actionTypeRegistry={actionTypeRegistry} - actionTypes={supportedActionTypes} - defaultActionMessage={DEFAULT_ACTION_MESSAGE} - toastNotifications={notifications.toasts} - /> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx deleted file mode 100644 index 2b1e5a367a965..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCard, - EuiFlexGrid, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { FieldHook } from '../../../../../shared_imports'; -import { useKibana } from '../../../../../lib/kibana'; -import * as i18n from './translations'; - -const MlCardDescription = ({ - subscriptionUrl, - hasValidLicense = false, -}: { - subscriptionUrl: string; - hasValidLicense?: boolean; -}) => ( - <EuiText size="s"> - {hasValidLicense ? ( - i18n.ML_TYPE_DESCRIPTION - ) : ( - <FormattedMessage - id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription" - defaultMessage="Access to ML requires a {subscriptionsLink}." - values={{ - subscriptionsLink: ( - <EuiLink href={subscriptionUrl} target="_blank"> - <FormattedMessage - id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink" - defaultMessage="Platinum subscription" - /> - </EuiLink> - ), - }} - /> - )} - </EuiText> -); - -interface SelectRuleTypeProps { - describedByIds?: string[]; - field: FieldHook; - hasValidLicense?: boolean; - isMlAdmin?: boolean; - isReadOnly?: boolean; -} - -export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ - describedByIds = [], - field, - isReadOnly = false, - hasValidLicense = false, - isMlAdmin = false, -}) => { - const ruleType = field.value as RuleType; - const setType = useCallback( - (type: RuleType) => { - field.setValue(type); - }, - [field] - ); - const setMl = useCallback(() => setType('machine_learning'), [setType]); - const setQuery = useCallback(() => setType('query'), [setType]); - const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; - const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { - path: '#/management/elasticsearch/license_management', - }); - - return ( - <EuiFormRow - fullWidth - data-test-subj="selectRuleType" - describedByIds={describedByIds} - label={field.label} - > - <EuiFlexGrid columns={4}> - <EuiFlexItem> - <EuiCard - data-test-subj="customRuleType" - title={i18n.QUERY_TYPE_TITLE} - description={i18n.QUERY_TYPE_DESCRIPTION} - icon={<EuiIcon size="l" type="search" />} - selectable={{ - isDisabled: isReadOnly, - onClick: setQuery, - isSelected: !isMlRule(ruleType), - }} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiCard - data-test-subj="machineLearningRuleType" - title={i18n.ML_TYPE_TITLE} - description={ - <MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} /> - } - icon={<EuiIcon size="l" type="machineLearningApp" />} - isDisabled={mlCardDisabled} - selectable={{ - isDisabled: mlCardDisabled, - onClick: setMl, - isSelected: isMlRule(ruleType), - }} - /> - </EuiFlexItem> - </EuiFlexGrid> - </EuiFormRow> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx deleted file mode 100644 index be9e919b806b5..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; -import React, { FC, memo, useCallback, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../../../plugins/siem/common/constants'; -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue } from '../../helpers'; -import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { QueryBarDefineRule } from '../query_bar'; -import { SelectRuleType } from '../select_rule_type'; -import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; -import { MlJobSelect } from '../ml_job_select'; -import { PickTimeline } from '../pick_timeline'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { NextStep } from '../next_step'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, - FormSchema, -} from '../../../../../shared_imports'; -import { schema } from './schema'; -import * as i18n from './translations'; -import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; -import { hasMlAdminPermissions } from '../../../../../components/ml/permissions/has_ml_admin_permissions'; - -const CommonUseField = getUseField({ component: Field }); - -interface StepDefineRuleProps extends RuleStepProps { - defaultValues?: DefineStepRule | null; -} - -const stepDefineDefaultValue: DefineStepRule = { - anomalyThreshold: 50, - index: [], - isNew: true, - machineLearningJobId: '', - ruleType: 'query', - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: null, - title: DEFAULT_TIMELINE_TITLE, - }, -}; - -const MyLabelButton = styled(EuiButtonEmpty)` - height: 18px; - font-size: 12px; - - .euiIcon { - width: 14px; - height: 14px; - } -`; - -MyLabelButton.defaultProps = { - flush: 'right', -}; - -const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ - addPadding = false, - defaultValues, - descriptionColumns = 'singleSplit', - isReadOnlyView, - isLoading, - isUpdateView = false, - setForm, - setStepData, -}) => { - const mlCapabilities = useMlCapabilities(); - const [openTimelineSearch, setOpenTimelineSearch] = useState(false); - const [indexModified, setIndexModified] = useState(false); - const [localIsMlRule, setIsMlRule] = useState(false); - const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const [myStepData, setMyStepData] = useState<DefineStepRule>({ - ...stepDefineDefaultValue, - index: indicesConfig ?? [], - }); - const [ - { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - ] = useFetchIndexPatterns(myStepData.index); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid && setStepData) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } - } - }, [form]); - - useEffect(() => { - const { isNew, ...values } = myStepData; - if (defaultValues != null && !deepEqual(values, defaultValues)) { - const newValues = { ...values, ...defaultValues, isNew: false }; - setMyStepData(newValues); - setFieldValue(form, schema, newValues); - } - }, [defaultValues, setMyStepData, setFieldValue]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.defineRule, form); - } - }, [form]); - - const handleResetIndices = useCallback(() => { - const indexField = form.getFields().index; - indexField.setValue(indicesConfig); - }, [form, indicesConfig]); - - const handleOpenTimelineSearch = useCallback(() => { - setOpenTimelineSearch(true); - }, []); - - const handleCloseTimelineSearch = useCallback(() => { - setOpenTimelineSearch(false); - }, []); - - return isReadOnlyView ? ( - <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}> - <StepRuleDescription - columns={descriptionColumns} - indexPatterns={indexPatternQueryBar as IIndexPattern} - schema={filterRuleFieldsForType(schema as FormSchema & RuleFields, myStepData.ruleType)} - data={filterRuleFieldsForType(myStepData, myStepData.ruleType)} - /> - </StepContentWrapper> - ) : ( - <> - <StepContentWrapper addPadding={!isUpdateView}> - <Form form={form} data-test-subj="stepDefineRule"> - <UseField - path="ruleType" - component={SelectRuleType} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRuleType'], - isReadOnly: isUpdateView, - hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, - isMlAdmin: hasMlAdminPermissions(mlCapabilities), - }} - /> - <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'none' : 'flex' }}> - <> - <CommonUseField - path="index" - config={{ - ...schema.index, - labelAppend: indexModified ? ( - <MyLabelButton onClick={handleResetIndices} iconType="refresh"> - {i18n.RESET_DEFAULT_INDEX} - </MyLabelButton> - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, - placeholder: '', - }, - }} - /> - <UseField - path="queryBar" - config={{ - ...schema.queryBar, - labelAppend: ( - <MyLabelButton onClick={handleOpenTimelineSearch}> - {i18n.IMPORT_TIMELINE_QUERY} - </MyLabelButton> - ), - }} - component={QueryBarDefineRule} - componentProps={{ - browserFields, - idAria: 'detectionEngineStepDefineRuleQueryBar', - indexPattern: indexPatternQueryBar, - isDisabled: isLoading, - isLoading: indexPatternLoadingQueryBar, - dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', - openTimelineSearch, - onCloseTimelineSearch: handleCloseTimelineSearch, - }} - /> - </> - </EuiFormRow> - <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}> - <> - <UseField - path="machineLearningJobId" - component={MlJobSelect} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'], - }} - /> - <UseField - path="anomalyThreshold" - component={AnomalyThresholdSlider} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'], - }} - /> - </> - </EuiFormRow> - <UseField - path="timeline" - component={PickTimeline} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleTimeline', - isDisabled: isLoading, - dataTestSubj: 'detectionEngineStepDefineRuleTimeline', - }} - /> - <FormDataProvider pathsToWatch={['index', 'ruleType']}> - {({ index, ruleType }) => { - if (index != null) { - if (deepEqual(index, indicesConfig) && indexModified) { - setIndexModified(false); - } else if (!deepEqual(index, indicesConfig) && !indexModified) { - setIndexModified(true); - } - } - - if (isMlRule(ruleType) && !localIsMlRule) { - setIsMlRule(true); - clearErrors(); - } else if (!isMlRule(ruleType) && localIsMlRule) { - setIsMlRule(false); - clearErrors(); - } - - return null; - }} - </FormDataProvider> - </Form> - </StepContentWrapper> - - {!isUpdateView && ( - <NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} /> - )} - </> - ); -}; - -export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx deleted file mode 100644 index 629c6758a1414..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; - -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; -import { FieldValueQueryBar } from '../query_bar'; -import { - ERROR_CODE, - FIELD_TYPES, - fieldValidators, - FormSchema, - ValidationFunc, -} from '../../../../../shared_imports'; -import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; - -export const schema: FormSchema = { - index: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', - { - defaultMessage: 'Index patterns', - } - ), - helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>, - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ formData }] = args; - const needsValidation = !isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - )(...args); - }, - }, - ], - }, - queryBar: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', - { - defaultMessage: 'Custom query', - } - ), - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ value, path, formData }] = args; - const { query, filters } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - return isEmpty(query.query as string) && isEmpty(filters) - ? { - code: 'ERR_FIELD_MISSING', - path, - message: CUSTOM_QUERY_REQUIRED, - } - : undefined; - }, - }, - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ value, path, formData }] = args; - const { query } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - if (!isEmpty(query.query as string) && query.language === 'kuery') { - try { - esKuery.fromKueryExpression(query.query); - } catch (err) { - return { - code: 'ERR_FIELD_FORMAT', - path, - message: INVALID_CUSTOM_QUERY, - }; - } - } - }, - }, - ], - }, - ruleType: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', - { - defaultMessage: 'Rule type', - } - ), - validations: [], - }, - anomalyThreshold: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', - { - defaultMessage: 'Anomaly score threshold', - } - ), - validations: [], - }, - machineLearningJobId: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', - { - defaultMessage: 'Machine Learning job', - } - ), - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ formData }] = args; - const needsValidation = isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', - { - defaultMessage: 'A Machine Learning job is required.', - } - ) - )(...args); - }, - }, - ], - }, - timeline: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', - { - defaultMessage: 'Timeline template', - } - ), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', - { - defaultMessage: - 'Select an existing timeline to use as a template when investigating generated signals.', - } - ), - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx deleted file mode 100644 index 3b297a623e34d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; - -import { - NOTIFICATION_THROTTLE_RULE, - NOTIFICATION_THROTTLE_NO_ACTIONS, -} from '../../../../../../../../../plugins/siem/common/constants'; -import { SelectField } from '../../../../../shared_imports'; - -export const THROTTLE_OPTIONS = [ - { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, - { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, - { value: '1h', text: 'Hourly' }, - { value: '1d', text: 'Daily' }, - { value: '7d', text: 'Weekly' }, -]; - -type ThrottleSelectField = typeof SelectField; - -export const ThrottleSelectField: ThrottleSelectField = props => { - const onChange = useCallback( - e => { - const throttle = e.target.value; - props.field.setValue(throttle); - props.handleChange(throttle); - }, - [props.field.setValue, props.handleChange] - ); - const newEuiFieldProps = { ...props.euiFieldProps, onChange }; - return <SelectField {...props} euiFieldProps={newEuiFieldProps} />; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts deleted file mode 100644 index a65e8178f75c4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { has, isEmpty } from 'lodash/fp'; -import moment from 'moment'; -import deepmerge from 'deepmerge'; - -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../plugins/siem/common/constants'; -import { transformAlertToRuleAction } from '../../../../../../../../plugins/siem/common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../../../../plugins/siem/common/detection_engine/types'; -import { isMlRule } from '../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { NewRule } from '../../../../containers/detection_engine/rules'; - -import { - AboutStepRule, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, - DefineStepRuleJson, - ScheduleStepRuleJson, - AboutStepRuleJson, - ActionsStepRuleJson, -} from '../types'; - -export const getTimeTypeValue = (time: string): { unit: string; value: number } => { - const timeObj = { - unit: '', - value: 0, - }; - const filterTimeVal = (time as string).match(/\d+/g); - const filterTimeType = (time as string).match(/[a-zA-Z]+/g); - if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { - timeObj.value = Number(filterTimeVal[0]); - } - if ( - !isEmpty(filterTimeType) && - filterTimeType != null && - ['s', 'm', 'h'].includes(filterTimeType[0]) - ) { - timeObj.unit = filterTimeType[0]; - } - return timeObj; -}; - -export interface RuleFields { - anomalyThreshold: unknown; - machineLearningJobId: unknown; - queryBar: unknown; - index: unknown; - ruleType: unknown; -} -type QueryRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId'>; -type MlRuleFields<T> = Omit<T, 'queryBar' | 'index'>; - -const isMlFields = <T>(fields: QueryRuleFields<T> | MlRuleFields<T>): fields is MlRuleFields<T> => - has('anomalyThreshold', fields); - -export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: RuleType) => { - if (isMlRule(type)) { - const { index, queryBar, ...mlRuleFields } = fields; - return mlRuleFields; - } else { - const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; - return queryRuleFields; - } -}; - -export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); - const { ruleType, timeline } = ruleFields; - const baseFields = { - type: ruleType, - ...(timeline.id != null && - timeline.title != null && { - timeline_id: timeline.id, - timeline_title: timeline.title, - }), - }; - - const typeFields = isMlFields(ruleFields) - ? { - anomaly_threshold: ruleFields.anomalyThreshold, - machine_learning_job_id: ruleFields.machineLearningJobId, - } - : { - index: ruleFields.index, - filters: ruleFields.queryBar?.filters, - language: ruleFields.queryBar?.query?.language, - query: ruleFields.queryBar?.query?.query as string, - saved_id: ruleFields.queryBar?.saved_id, - ...(ruleType === 'query' && - ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), - }; - - return { - ...baseFields, - ...typeFields, - }; -}; - -export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { - const { isNew, ...formatScheduleData } = scheduleData; - if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { - const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( - formatScheduleData.interval - ); - const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); - const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); - duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); - formatScheduleData.from = `now-${duration.asSeconds()}s`; - formatScheduleData.to = 'now'; - } - return { - ...formatScheduleData, - meta: { - from: scheduleData.from, - }, - }; -}; - -export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; - return { - false_positives: falsePositives.filter(item => !isEmpty(item)), - references: references.filter(item => !isEmpty(item)), - risk_score: riskScore, - threat: threat - .filter(singleThreat => singleThreat.tactic.name !== 'none') - .map(singleThreat => ({ - ...singleThreat, - framework: 'MITRE ATT&CK', - technique: singleThreat.technique.map(technique => { - const { id, name, reference } = technique; - return { id, name, reference }; - }), - })), - ...(!isEmpty(note) ? { note } : {}), - ...rest, - }; -}; - -export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; - - return { - actions: actions.map(transformAlertToRuleAction), - enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, - meta: { - kibana_siem_app_url: kibanaSiemAppUrl, - }, - }; -}; - -export const formatRule = ( - defineStepData: DefineStepRule, - aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule, - actionsData: ActionsStepRule -): NewRule => - deepmerge.all([ - formatDefineStepData(defineStepData), - formatAboutStepData(aboutStepData), - formatScheduleStepData(scheduleData), - formatActionsStepData(actionsData), - ]) as NewRule; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx deleted file mode 100644 index 1c01a19573cd6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - GetStepsData, - getDefineStepsData, - getScheduleStepsData, - getStepsData, - getAboutStepsData, - getActionsStepsData, - getHumanizedDuration, - getModifiedAboutDetailsData, - determineDetailsValue, - userHasNoPermissions, -} from './helpers'; -import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -describe('rule helpers', () => { - describe('getStepsData', () => { - test('returns object with about, define, schedule and actions step properties formatted', () => { - const { - defineRuleData, - modifiedAboutRuleDetailsData, - aboutRuleData, - scheduleRuleData, - ruleActionsData, - }: GetStepsData = getStepsData({ - rule: mockRuleWithEverything('test-id'), - }); - const defineRuleStepData = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - index: ['auditbeat-*'], - machineLearningJobId: '', - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, - }; - const aboutRuleStepData = { - description: '24/7', - falsePositives: ['test'], - isNew: false, - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - riskScore: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; - const ruleActionsStepData = { - enabled: true, - throttle: 'no_actions', - isNew: false, - actions: [], - }; - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(defineRuleData).toEqual(defineRuleStepData); - expect(aboutRuleData).toEqual(aboutRuleStepData); - expect(scheduleRuleData).toEqual(scheduleRuleStepData); - expect(ruleActionsData).toEqual(ruleActionsStepData); - expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); - }); - }); - - describe('getAboutStepsData', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); - - expect(result.name).toEqual(''); - expect(result.description).toEqual(''); - expect(result.note).toEqual(''); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: AboutStepRule = getAboutStepsData(mockedRule, false); - - expect(result.note).toEqual(''); - }); - }); - - describe('determineDetailsValue', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockRuleWithEverything('test-id'), - true - ); - const expected = { name: '', description: '', note: '' }; - - expect(result).toEqual(expected); - }); - - test('returns name, description, and note values if detailsView is false', () => { - const mockedRule = mockRuleWithEverything('test-id'); - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockedRule, - false - ); - const expected = { - name: mockedRule.name, - description: mockedRule.description, - note: mockedRule.note, - }; - - expect(result).toEqual(expected); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockedRule, - false - ); - const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; - - expect(result).toEqual(expected); - }); - }); - - describe('getDefineStepsData', () => { - test('returns with saved_id if value exists on rule', () => { - const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: "Garrett's IP", - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns with saved_id of undefined if value does not exist on rule', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - delete mockedRule.saved_id; - const result: DefineStepRule = getDefineStepsData(mockedRule); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns timeline id and title of null if they do not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.timeline_id; - delete mockedRule.timeline_title; - const result: DefineStepRule = getDefineStepsData(mockedRule); - - expect(result.timeline.id).toBeNull(); - expect(result.timeline.title).toBeNull(); - }); - }); - - describe('getHumanizedDuration', () => { - test('returns from as seconds if from duration is less than a minute', () => { - const result = getHumanizedDuration('now-62s', '1m'); - - expect(result).toEqual('2s'); - }); - - test('returns from as minutes if from duration is less than an hour', () => { - const result = getHumanizedDuration('now-660s', '5m'); - - expect(result).toEqual('6m'); - }); - - test('returns from as hours if from duration is more than 60 minutes', () => { - const result = getHumanizedDuration('now-7400s', '5m'); - - expect(result).toEqual('1h'); - }); - - test('returns from as if from is not parsable as dateMath', () => { - const result = getHumanizedDuration('randomstring', '5m'); - - expect(result).toEqual('NaNh'); - }); - - test('returns from as 5m if interval is not parsable as dateMath', () => { - const result = getHumanizedDuration('now-300s', 'randomstring'); - - expect(result).toEqual('5m'); - }); - }); - - describe('getScheduleStepsData', () => { - test('returns expected ScheduleStep rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - const result: ScheduleStepRule = getScheduleStepsData(mockedRule); - const expected = { - isNew: false, - interval: mockedRule.interval, - from: '0s', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getActionsStepsData', () => { - test('returns expected ActionsStepRule rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], - }; - const result: ActionsStepRule = getActionsStepsData(mockedRule); - const expected = { - actions: [ - { - id: 'id', - group: 'group', - params: {}, - actionTypeId: 'action_type_id', - }, - ], - enabled: mockedRule.enabled, - isNew: false, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getModifiedAboutDetailsData', () => { - test('returns object with "note" and "description" being those of passed in rule', () => { - const result: AboutStepRuleDetails = getModifiedAboutDetailsData( - mockRuleWithEverything('test-id') - ); - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(result).toEqual(aboutRuleDataDetailsData); - }); - - test('returns "note" with empty string if "note" does not exist', () => { - const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; - const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); - - const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; - - expect(result).toEqual(aboutRuleDetailsData); - }); - }); - - describe('userHasNoPermissions', () => { - test("returns false when user's CRUD operations are null", () => { - const result: boolean = userHasNoPermissions(null); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns true when user cannot CRUD', () => { - const result: boolean = userHasNoPermissions(false); - const userHasNoPermissionsExpectedResult = true; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns false when user can CRUD', () => { - const result: boolean = userHasNoPermissions(true); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx deleted file mode 100644 index 7bea41c2ab4d5..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import dateMath from '@elastic/datemath'; -import { get } from 'lodash/fp'; -import moment from 'moment'; -import memoizeOne from 'memoize-one'; -import { useLocation } from 'react-router-dom'; - -import { - RuleAlertAction, - RuleType, -} from '../../../../../../../plugins/siem/common/detection_engine/types'; -import { isMlRule } from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { transformRuleToAlertAction } from '../../../../../../../plugins/siem/common/detection_engine/transform_actions'; -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from '../../../shared_imports'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - IMitreEnterpriseAttack, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -export interface GetStepsData { - aboutRuleData: AboutStepRule; - modifiedAboutRuleDetailsData: AboutStepRuleDetails; - defineRuleData: DefineStepRule; - scheduleRuleData: ScheduleStepRule; - ruleActionsData: ActionsStepRule; -} - -export const getStepsData = ({ - rule, - detailsView = false, -}: { - rule: Rule; - detailsView?: boolean; -}): GetStepsData => { - const defineRuleData: DefineStepRule = getDefineStepsData(rule); - const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); - const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); - const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); - const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); - - return { - aboutRuleData, - modifiedAboutRuleDetailsData, - defineRuleData, - scheduleRuleData, - ruleActionsData, - }; -}; - -export const getActionsStepsData = ( - rule: Omit<Rule, 'actions'> & { actions: RuleAlertAction[] } -): ActionsStepRule => { - const { enabled, throttle, meta, actions = [] } = rule; - - return { - actions: actions?.map(transformRuleToAlertAction), - isNew: false, - throttle, - kibanaSiemAppUrl: meta?.kibana_siem_app_url, - enabled, - }; -}; - -export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ - isNew: false, - ruleType: rule.type, - anomalyThreshold: rule.anomaly_threshold ?? 50, - machineLearningJobId: rule.machine_learning_job_id ?? '', - index: rule.index ?? [], - queryBar: { - query: { query: rule.query ?? '', language: rule.language ?? '' }, - filters: (rule.filters ?? []) as Filter[], - saved_id: rule.saved_id, - }, - timeline: { - id: rule.timeline_id ?? null, - title: rule.timeline_title ?? null, - }, -}); - -export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { - const { interval, from } = rule; - const fromHumanizedValue = getHumanizedDuration(from, interval); - - return { - isNew: false, - interval, - from: fromHumanizedValue, - }; -}; - -export const getHumanizedDuration = (from: string, interval: string): string => { - const fromValue = dateMath.parse(from) ?? moment(); - const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); - - const fromDuration = moment.duration(intervalValue.diff(fromValue)); - const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; - - if (fromDuration.asSeconds() < 60) { - return `${Math.floor(fromDuration.asSeconds())}s`; - } else if (fromDuration.asMinutes() < 60) { - return `${Math.floor(fromDuration.asMinutes())}m`; - } - - return fromHumanize; -}; - -export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { - const { name, description, note } = determineDetailsValue(rule, detailsView); - const { - references, - severity, - false_positives: falsePositives, - risk_score: riskScore, - tags, - threat, - } = rule; - - return { - isNew: false, - name, - description, - note: note!, - references, - severity, - tags, - riskScore, - falsePositives, - threat: threat as IMitreEnterpriseAttack[], - }; -}; - -export const determineDetailsValue = ( - rule: Rule, - detailsView: boolean -): Pick<Rule, 'name' | 'description' | 'note'> => { - const { name, description, note } = rule; - if (detailsView) { - return { name: '', description: '', note: '' }; - } - - return { name, description, note: note ?? '' }; -}; - -export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ - note: rule.note ?? '', - description: rule.description, -}); - -export const useQuery = () => new URLSearchParams(useLocation().search); - -export type PrePackagedRuleStatus = - | 'ruleInstalled' - | 'ruleNotInstalled' - | 'ruleNeedUpdate' - | 'someRuleUninstall' - | 'unknown'; - -export const getPrePackagedRuleStatus = ( - rulesInstalled: number | null, - rulesNotInstalled: number | null, - rulesNotUpdated: number | null -): PrePackagedRuleStatus => { - if ( - rulesNotInstalled != null && - rulesInstalled === 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'ruleNotInstalled'; - } else if ( - rulesInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled === 0 && - rulesNotUpdated === 0 - ) { - return 'ruleInstalled'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'someRuleUninstall'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesNotUpdated != null && - rulesInstalled > 0 && - rulesNotInstalled >= 0 && - rulesNotUpdated > 0 - ) { - return 'ruleNeedUpdate'; - } - return 'unknown'; -}; -export const setFieldValue = ( - form: FormHook<FormData>, - schema: FormSchema<FormData>, - defaultValues: unknown -) => - Object.keys(schema).forEach(key => { - const val = get(key, defaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - -export const redirectToDetections = ( - isSignalIndexExists: boolean | null, - isAuthenticated: boolean | null, - hasEncryptionKey: boolean | null -) => - isSignalIndexExists != null && - isAuthenticated != null && - hasEncryptionKey != null && - (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); - -export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { - const commonRuleParamsKeys = [ - 'id', - 'name', - 'description', - 'false_positives', - 'rule_id', - 'max_signals', - 'risk_score', - 'output_index', - 'references', - 'severity', - 'timeline_id', - 'timeline_title', - 'threat', - 'type', - 'version', - // 'lists', - ]; - - const ruleParamsKeys = [ - ...commonRuleParamsKeys, - ...(isMlRule(ruleType) - ? ['anomaly_threshold', 'machine_learning_job_id'] - : ['index', 'filters', 'language', 'query', 'saved_id']), - ].sort(); - - return ruleParamsKeys; -}; - -export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { - if (!ruleType) { - return []; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - 'state.signals_count', - '{context.results_link}', - ...actionMessageRuleParams.map(param => `context.rule.${param}`), - ]; -}); - -// typed as null not undefined as the initial state for this value is null. -export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => - canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts deleted file mode 100644 index 380ef52190349..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - RuleAlertAction, - RuleType, -} from '../../../../../../../plugins/siem/common/detection_engine/types'; -import { AlertAction } from '../../../../../../../plugins/alerting/common'; -import { Filter } from '../../../../../../../../src/plugins/data/common'; -import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from '../../../shared_imports'; -import { FieldValueTimeline } from './components/pick_timeline'; - -export interface EuiBasicTableSortTypes { - field: string; - direction: 'asc' | 'desc'; -} - -export interface EuiBasicTableOnChange { - page: { - index: number; - size: number; - }; - sort?: EuiBasicTableSortTypes; -} - -export enum RuleStep { - defineRule = 'define-rule', - aboutRule = 'about-rule', - scheduleRule = 'schedule-rule', - ruleActions = 'rule-actions', -} -export type RuleStatusType = 'passive' | 'active' | 'valid'; - -export interface RuleStepData { - data: unknown; - isValid: boolean; -} - -export interface RuleStepProps { - addPadding?: boolean; - descriptionColumns?: 'multi' | 'single' | 'singleSplit'; - setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; - isReadOnlyView: boolean; - isUpdateView?: boolean; - isLoading: boolean; - resizeParentContainer?: (height: number) => void; - setForm?: (step: RuleStep, form: FormHook<FormData>) => void; -} - -interface StepRuleData { - isNew: boolean; -} -export interface AboutStepRule extends StepRuleData { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note: string; -} - -export interface AboutStepRuleDetails { - note: string; - description: string; -} - -export interface DefineStepRule extends StepRuleData { - anomalyThreshold: number; - index: string[]; - machineLearningJobId: string; - queryBar: FieldValueQueryBar; - ruleType: RuleType; - timeline: FieldValueTimeline; -} - -export interface ScheduleStepRule extends StepRuleData { - interval: string; - from: string; - to?: string; -} - -export interface ActionsStepRule extends StepRuleData { - actions: AlertAction[]; - enabled: boolean; - kibanaSiemAppUrl?: string; - throttle?: string | null; -} - -export interface DefineStepRuleJson { - anomaly_threshold?: number; - index?: string[]; - filters?: Filter[]; - machine_learning_job_id?: string; - saved_id?: string; - query?: string; - language?: string; - timeline_id?: string; - timeline_title?: string; - type: RuleType; -} - -export interface AboutStepRuleJson { - name: string; - description: string; - severity: string; - risk_score: number; - references: string[]; - false_positives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note?: string; -} - -export interface ScheduleStepRuleJson { - interval: string; - from: string; - to?: string; - meta?: unknown; -} - -export interface ActionsStepRuleJson { - actions: RuleAlertAction[]; - enabled: boolean; - throttle?: string | null; - meta?: unknown; -} - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} -export interface IMitreEnterpriseAttack { - framework: string; - tactic: IMitreAttack; - technique: IMitreAttack[]; -} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts deleted file mode 100644 index 11942c57a4d27..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { - getDetectionEngineUrl, - getDetectionEngineTabUrl, - getRulesUrl, - getRuleDetailsUrl, - getCreateRuleUrl, - getEditRuleUrl, -} from '../../../components/link_to/redirect_to_detection_engine'; -import * as i18nDetections from '../translations'; -import * as i18nRules from './translations'; -import { RouteSpyState } from '../../../utils/route/types'; - -const getTabBreadcrumb = (pathname: string, search: string[]) => { - const tabPath = pathname.split('/')[2]; - - if (tabPath === 'alerts') { - return { - text: i18nDetections.ALERT, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'signals') { - return { - text: i18nDetections.SIGNAL, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'rules') { - return { - text: i18nRules.PAGE_TITLE, - href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } -}; - -const isRuleCreatePage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/create'); - -const isRuleEditPage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/edit'); - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18nDetections.PAGE_TITLE, - href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); - - if (tabBreadcrumb) { - breadcrumb = [...breadcrumb, tabBreadcrumb]; - } - - if (params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state.ruleName, - href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleCreatePage(params.pathName)) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.ADD_PAGE_TITLE, - href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.EDIT_PAGE_TITLE, - href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx deleted file mode 100644 index a8a34383585c6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import styled from 'styled-components'; - -import { useThrottledResizeObserver } from '../../components/utils'; -import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; -import { Flyout } from '../../components/flyout'; -import { HeaderGlobal } from '../../components/header_global'; -import { HelpMenu } from '../../components/help_menu'; -import { LinkToPage } from '../../components/link_to'; -import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; -import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; -import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; -import { UseUrlState } from '../../components/url_state'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; -import { NotFoundPage } from '../404'; -import { DetectionEngineContainer } from '../detection_engine'; -import { HostsContainer } from '../hosts'; -import { NetworkContainer } from '../network'; -import { Overview } from '../overview'; -import { Case } from '../case'; -import { Timelines } from '../timelines'; -import { navTabs } from './home_navigations'; -import { SiemPageName } from './types'; - -/* - * This is import is important to keep because if we do not have it - * we will loose the map embeddable until they move to the New Platform - * we need to have it - */ -import 'uiExports/embeddableFactories'; - -const WrappedByAutoSizer = styled.div` - height: 100%; -`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - -const Main = styled.main` - height: 100%; -`; -Main.displayName = 'Main'; - -const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) - -/** the global Kibana navigation at the top of every page */ -const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - -export const HomePage: React.FC = () => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); - - const [showTimeline] = useShowTimeline(); - - return ( - <WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" ref={measureRef}> - <HeaderGlobal /> - - <Main data-test-subj="pageContainer"> - <WithSource sourceId="default"> - {({ browserFields, indexPattern, indicesExist }) => ( - <DragDropContextWrapper browserFields={browserFields}> - <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( - <> - <AutoSaveWarningMsg /> - <Flyout - flyoutHeight={flyoutHeight} - timelineId="timeline-1" - usersViewing={usersViewing} - /> - </> - )} - - <Switch> - <Redirect exact from="/" to={`/${SiemPageName.overview}`} /> - <Route path={`/:pageName(${SiemPageName.overview})`} render={() => <Overview />} /> - <Route - path={`/:pageName(${SiemPageName.hosts})`} - render={({ match }) => <HostsContainer url={match.url} />} - /> - <Route - path={`/:pageName(${SiemPageName.network})`} - render={({ location, match }) => ( - <NetworkContainer location={location} url={match.url} /> - )} - /> - <Route - path={`/:pageName(${SiemPageName.detections})`} - render={({ location, match }) => ( - <DetectionEngineContainer location={location} url={match.url} /> - )} - /> - <Route - path={`/:pageName(${SiemPageName.timelines})`} - render={() => <Timelines />} - /> - <Route path="/link-to" render={props => <LinkToPage {...props} />} /> - <Route - path="/ml-hosts" - render={({ location, match }) => ( - <MlHostConditionalContainer location={location} url={match.url} /> - )} - /> - <Route - path="/ml-network" - render={({ location, match }) => ( - <MlNetworkConditionalContainer location={location} url={match.url} /> - )} - /> - <Route path={`/:pageName(${SiemPageName.case})`}> - <Case /> - </Route> - <Route render={() => <NotFoundPage />} /> - </Switch> - </DragDropContextWrapper> - )} - </WithSource> - </Main> - - <HelpMenu /> - - <SpyRoute /> - </WrappedByAutoSizer> - ); -}; - -HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts deleted file mode 100644 index 69268d841e12e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getHostDetailsEventsKqlQueryExpression, getHostDetailsPageFilters } from './helpers'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; - -describe('hosts page helpers', () => { - describe('getHostDetailsEventsKqlQueryExpression', () => { - const filterQueryExpression = 'user.name: "root"'; - const hostName = 'foo'; - - it('combines the filterQueryExpression and hostname when both are NOT empty', () => { - expect(getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName })).toEqual( - 'user.name: "root" and host.name: "foo"' - ); - }); - - it('returns just the filterQueryExpression when it is NOT empty, but hostname is empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName: '' }) - ).toEqual('user.name: "root"'); - }); - - it('returns just the hostname when filterQueryExpression is empty, but hostname is NOT empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName }) - ).toEqual('host.name: "foo"'); - }); - - it('returns an empty string when both the filterQueryExpression and hostname are empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName: '' }) - ).toEqual(''); - }); - }); - - describe('getHostDetailsPageFilters', () => { - it('correctly constructs pageFilters for the given hostName', () => { - const expected: Filter[] = [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: 'host-1', - params: { - query: 'host-1', - }, - }, - query: { - match: { - 'host.name': { - query: 'host-1', - type: 'phrase', - }, - }, - }, - }, - ]; - expect(getHostDetailsPageFilters('host-1')).toEqual(expected); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts deleted file mode 100644 index 461fde2bba0ca..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { escapeQueryValue } from '../../../lib/keury'; -import { Filter } from '../../../../../../../../src/plugins/data/public'; - -/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ -export const getHostDetailsEventsKqlQueryExpression = ({ - filterQueryExpression, - hostName, -}: { - filterQueryExpression: string; - hostName: string; -}): string => { - if (filterQueryExpression.length) { - return `${filterQueryExpression}${ - hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' - }`; - } else { - return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; - } -}; - -export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: hostName, - params: { - query: hostName, - }, - }, - query: { - match: { - 'host.name': { - query: hostName, - type: 'phrase', - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx deleted file mode 100644 index a12c95b3b5a6f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; -import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../components/ml_popover/hooks/use_ml_capabilities'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { SiemNavigation } from '../../../components/navigation'; -import { KpiHostsComponent } from '../../../components/page/hosts'; -import { HostOverview } from '../../../components/page/hosts/host_overview'; -import { manageQuery } from '../../../components/page/manage_query'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; -import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { inputsSelectors, State } from '../../../store'; -import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { esQuery, Filter } from '../../../../../../../../src/plugins/data/public'; - -import { HostsEmptyPage } from '../hosts_empty_page'; -import { HostDetailsTabs } from './details_tabs'; -import { navTabsHostDetails } from './nav_tabs'; -import { HostDetailsProps } from './types'; -import { type } from './utils'; -import { getHostDetailsPageFilters } from './helpers'; - -const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); - -const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>( - ({ - filters, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero, - setQuery, - to, - detailName, - deleteQuery, - hostDetailsPagePath, - }) => { - useEffect(() => { - setHostDetailsTablesActivePageToZero(); - }, [setHostDetailsTablesActivePageToZero, detailName]); - const capabilities = useMlCapabilities(); - const kibana = useKibana(); - const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ - detailName, - ]); - const getFilters = () => [...hostDetailsPageFilters, ...filters]; - const narrowDateRange = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - return ( - <> - <WithSource sourceId="default"> - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <StickyContainer> - <FiltersGlobal> - <SiemSearchBar indexPattern={indexPattern} id="global" /> - </FiltersGlobal> - - <WrapperPage> - <HeaderPage - border - subtitle={ - <LastEventTime - indexKey={LastEventIndexKey.hostDetails} - hostName={detailName} - /> - } - title={detailName} - /> - - <HostOverviewByNameQuery - sourceId="default" - hostName={detailName} - skip={isInitializing} - startDate={from} - endDate={to} - > - {({ hostOverview, loading, id, inspect, refetch }) => ( - <AnomalyTableProvider - criteriaFields={hostToCriteria(hostOverview)} - startDate={from} - endDate={to} - skip={isInitializing} - > - {({ isLoadingAnomaliesData, anomaliesData }) => ( - <HostOverviewManage - id={id} - inspect={inspect} - refetch={refetch} - setQuery={setQuery} - data={hostOverview} - anomaliesData={anomaliesData} - isLoadingAnomaliesData={isLoadingAnomaliesData} - loading={loading} - startDate={from} - endDate={to} - narrowDateRange={(score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - </AnomalyTableProvider> - )} - </HostOverviewByNameQuery> - - <EuiHorizontalRule /> - - <KpiHostDetailsQuery - sourceId="default" - filterQuery={filterQuery} - skip={isInitializing} - startDate={from} - endDate={to} - > - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - <KpiHostDetailsManage - data={kpiHostDetails} - from={from} - id={id} - inspect={inspect} - loading={loading} - refetch={refetch} - setQuery={setQuery} - to={to} - narrowDateRange={narrowDateRange} - /> - )} - </KpiHostDetailsQuery> - - <EuiSpacer /> - - <SiemNavigation - navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))} - /> - - <EuiSpacer /> - - <HostDetailsTabs - isInitializing={isInitializing} - deleteQuery={deleteQuery} - pageFilters={hostDetailsPageFilters} - to={to} - from={from} - detailName={detailName} - type={type} - setQuery={setQuery} - filterQuery={filterQuery} - hostDetailsPagePath={hostDetailsPagePath} - indexPattern={indexPattern} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} - /> - </WrapperPage> - </StickyContainer> - ) : ( - <WrapperPage> - <HeaderPage border title={detailName} /> - - <HostsEmptyPage /> - </WrapperPage> - ); - }} - </WithSource> - - <SpyRoute /> - </> - ); - } -); -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -export const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts deleted file mode 100644 index c4680dc3d795e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { hostsModel } from '../../../store'; -import { HostsTableType } from '../../../store/hosts/model'; -import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; - -import * as i18n from '../translations'; -import { HostRouteSpyState } from '../../../utils/route/types'; - -export const type = hostsModel.HostsType.details; - -const TabNameMappedToI18nKey: Record<HostsTableType, string> = { - [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, - [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, - [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, -}; - -export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - if (params.tabName != null) { - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index ebcb07131bb24..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { AlertsComponentQueryProps } from './types'; - -export const filterHostData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', - }, - }, -]; -export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { - const { pageFilters, ...rest } = alertsProps; - const hostPageFilters = useMemo( - () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), - [pageFilters] - ); - - return <AlertsView {...rest} pageFilters={hostPageFilters} />; -}); - -HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts deleted file mode 100644 index 207b86fee02b9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; - -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { NavTab } from '../../../components/navigation/types'; -import { UpdateDateRange } from '../../../components/charts/common'; - -export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & - HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; - -type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; - -export type HostsNavTab = Record<KeyHostsNavTab, NavTab>; - -export type SetQuery = ({ - id, - inspect, - loading, - refetch, -}: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; -}) => void; - -export interface QueryTabBodyProps { - type: HostsType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; -} - -export type HostsComponentsQueryProps = QueryTabBodyProps & { - deleteQuery?: ({ id }: { id: string }) => void; - indexPattern: IIndexPattern; - pageFilters?: Filter[]; - skip: boolean; - setQuery: SetQuery; - updateDateRange?: UpdateDateRange; - narrowDateRange?: NarrowDateRange; -}; - -export type AlertsComponentQueryProps = HostsComponentsQueryProps & { - filterQuery: string; - pageFilters?: Filter[]; -}; - -export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx deleted file mode 100644 index e796eaca0cd28..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { networkToCriteria } from '../../../components/ml/criteria/network_to_criteria'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; -import { manageQuery } from '../../../components/page/manage_query'; -import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected'; -import { IpOverview } from '../../../components/page/network/ip_overview'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { IpOverviewQuery } from '../../../containers/ip_overview'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group'; -import { networkModel, State, inputsSelectors } from '../../../store'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { NetworkEmptyPage } from '../network_empty_page'; -import { NetworkHttpQueryTable } from './network_http_query_table'; -import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; -import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; -import { TlsQueryTable } from './tls_query_table'; -import { IPDetailsComponentProps } from './types'; -import { UsersQueryTable } from './users_query_table'; -import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; - -export { getBreadcrumbs } from './utils'; - -const IpOverviewManage = manageQuery(IpOverview); - -export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRedux> = ({ - detailName, - filters, - flowTarget, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero, - setQuery, - to, -}) => { - const type = networkModel.NetworkType.details; - const narrowDateRange = useCallback( - (score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ); - const kibana = useKibana(); - - useEffect(() => { - setIpDetailsTablesActivePageToZero(); - }, [detailName, setIpDetailsTablesActivePageToZero]); - - return ( - <> - <WithSource sourceId="default" data-test-subj="ip-details-page"> - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <StickyContainer> - <FiltersGlobal> - <SiemSearchBar indexPattern={indexPattern} id="global" /> - </FiltersGlobal> - - <WrapperPage> - <HeaderPage - border - data-test-subj="ip-details-headline" - draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} - subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />} - title={ip} - > - <FlowTargetSelectConnected flowTarget={flowTarget} /> - </HeaderPage> - - <IpOverviewQuery - skip={isInitializing} - sourceId="default" - filterQuery={filterQuery} - type={type} - ip={ip} - > - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - <AnomalyTableProvider - criteriaFields={networkToCriteria(detailName, flowTarget)} - startDate={from} - endDate={to} - skip={isInitializing} - > - {({ isLoadingAnomaliesData, anomaliesData }) => ( - <IpOverviewManage - id={id} - inspect={inspect} - ip={ip} - data={ipOverviewData} - anomaliesData={anomaliesData} - loading={loading} - isLoadingAnomaliesData={isLoadingAnomaliesData} - type={type} - flowTarget={flowTarget} - refetch={refetch} - setQuery={setQuery} - startDate={from} - endDate={to} - narrowDateRange={narrowDateRange} - /> - )} - </AnomalyTableProvider> - )} - </IpOverviewQuery> - - <EuiHorizontalRule /> - - <ConditionalFlexGroup direction="column"> - <EuiFlexItem> - <NetworkTopNFlowQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={FlowTargetSourceDest.source} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - - <EuiFlexItem> - <NetworkTopNFlowQueryTable - endDate={to} - flowTarget={FlowTargetSourceDest.destination} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - </ConditionalFlexGroup> - - <EuiSpacer /> - - <ConditionalFlexGroup direction="column"> - <EuiFlexItem> - <NetworkTopCountriesQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={FlowTargetSourceDest.source} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - - <EuiFlexItem> - <NetworkTopCountriesQueryTable - endDate={to} - flowTarget={FlowTargetSourceDest.destination} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - </ConditionalFlexGroup> - - <EuiSpacer /> - - <UsersQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={flowTarget} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - /> - - <EuiSpacer /> - - <NetworkHttpQueryTable - endDate={to} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - /> - - <EuiSpacer /> - - <TlsQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={(flowTarget as unknown) as FlowTargetSourceDest} - ip={ip} - setQuery={setQuery} - skip={isInitializing} - startDate={from} - type={type} - /> - - <EuiSpacer /> - - <AnomaliesQueryTabBody - filterQuery={filterQuery} - setQuery={setQuery} - startDate={from} - endDate={to} - skip={isInitializing} - ip={ip} - type={type} - flowTarget={flowTarget} - narrowDateRange={narrowDateRange} - hideHistogramIfEmpty={true} - AnomaliesTableComponent={AnomaliesNetworkTable} - /> - </WrapperPage> - </StickyContainer> - ) : ( - <WrapperPage> - <HeaderPage border title={ip} /> - - <NetworkEmptyPage /> - </WrapperPage> - ); - }} - </WithSource> - - <SpyRoute /> - </> - ); -}; -IPDetailsComponent.displayName = 'IPDetailsComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, -}; - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts deleted file mode 100644 index efd9c644ec6b6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IIndexPattern } from 'src/plugins/data/public'; - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { NetworkType } from '../../../store/network/model'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -export const type = NetworkType.details; - -export type IPDetailsComponentProps = GlobalTimeArgs & { - detailName: string; - flowTarget: FlowTarget; -}; - -export interface OwnProps { - type: NetworkType; - startDate: number; - endDate: number; - filterQuery: string | ESTermQuery; - ip: string; - skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; -} - -export type NetworkComponentsQueryProps = OwnProps & { - flowTarget: FlowTarget; -}; - -export type TlsQueryTableComponentProps = OwnProps & { - flowTarget: FlowTargetSourceDest; -}; - -export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { - flowTarget: FlowTargetSourceDest; - indexPattern: IIndexPattern; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts deleted file mode 100644 index 1488dd556c0b9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; -import { networkModel } from '../../../store/network'; -import * as i18n from '../translations'; -import { NetworkRouteType } from '../navigation/types'; -import { NetworkRouteSpyState } from '../../../utils/route/types'; - -export const type = networkModel.NetworkType.details; -const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { - [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, - [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, - [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, - [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, - [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, -}; - -export const getBreadcrumbs = ( - params: NetworkRouteSpyState, - search: string[] -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: decodeIpv6(params.detailName), - href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ - !isEmpty(search[1]) ? search[1] : '' - }`, - }, - ]; - } - - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index a5d0207f526d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { NetworkComponentQueryProps } from './types'; - -export const filterNetworkData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - bool: { - should: [ - { - exists: { - field: 'source.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - exists: { - field: 'destination.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', - }, - }, -]; - -export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( - <AlertsView {...alertsProps} pageFilters={filterNetworkData} /> -)); - -NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts deleted file mode 100644 index 90c18b6ff69f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/'; - -import { NavTab } from '../../../components/navigation/types'; -import { FlowTargetSourceDest } from '../../../graphql/types'; -import { networkModel } from '../../../store'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -import { SetAbsoluteRangeDatePicker } from '../types'; -import { NarrowDateRange } from '../../../components/ml/types'; - -interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { - skip: boolean; - type: networkModel.NetworkType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; - narrowDateRange?: NarrowDateRange; -} - -export type NetworkComponentQueryProps = QueryTabBodyProps; - -export type IPsQueryTabBodyProps = QueryTabBodyProps & { - indexPattern: IIndexPattern; - flowTarget: FlowTargetSourceDest; -}; - -export type TlsQueryTabBodyProps = QueryTabBodyProps & { - flowTarget: FlowTargetSourceDest; - ip?: string; -}; - -export type HttpQueryTabBodyProps = QueryTabBodyProps & { - ip?: string; -}; - -export type NetworkRoutesProps = GlobalTimeArgs & { - networkPagePath: string; - type: networkModel.NetworkType; - filterQuery?: string | ESTermQuery; - indexPattern: IIndexPattern; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; -}; - -export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & - NetworkRouteType.flows & - NetworkRouteType.http & - NetworkRouteType.tls & - NetworkRouteType.alerts; - -type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & - NetworkRouteType.anomalies; - -type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; - -export type NetworkNavTab = Record<KeyNetworkNavTab, NavTab>; - -export enum NetworkRouteType { - flows = 'flows', - dns = 'dns', - anomalies = 'anomalies', - tls = 'tls', - http = 'http', - alerts = 'alerts', -} - -export type GetNetworkRoutePath = ( - pagePath: string, - capabilitiesFetched: boolean, - hasMlUserPermission: boolean -) => string; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx deleted file mode 100644 index 8e09572cb2796..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import { Position } from '@elastic/charts'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; -import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; -import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsType } from '../../../store/hosts/model'; - -import * as i18n from '../translations'; -import { - alertsStackByOptions, - histogramConfigs, -} from '../../../components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; - -const ID = 'alertsByCategoryOverview'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.module'; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - hideHeaderChildren?: boolean; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const AlertsByCategoryComponent: React.FC<Props> = ({ - deleteQuery, - filters = NO_FILTERS, - from, - hideHeaderChildren = false, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const alertsCountViewAlertsButton = useMemo( - () => ( - <EuiButton data-test-subj="view-alerts" href={getDetectionEngineAlertUrl(urlSearch)}> - {i18n.VIEW_ALERTS} - </EuiButton> - ), - [urlSearch] - ); - - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - defaultStackByOption: - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - legendPosition: Position.Right, - }), - [] - ); - - return ( - <MatrixHistogramContainer - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - })} - headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} - id={ID} - setQuery={setQuery} - sourceId="default" - startDate={from} - type={HostsType.page} - {...alertsByCategoryHistogramConfigs} - /> - ); -}; - -AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; - -export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx deleted file mode 100644 index 0fc37935b6062..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { OverviewHost } from '../../../components/page/overview/overview_host'; -import { OverviewNetwork } from '../../../components/page/overview/overview_network'; -import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; - -const HorizontalSpacer = styled(EuiFlexItem)` - width: 24px; -`; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; - -interface Props { - filters?: Filter[]; - from: number; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const EventCountsComponent: React.FC<Props> = ({ - filters = NO_FILTERS, - from, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - const kibana = useKibana(); - - return ( - <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween"> - <EuiFlexItem grow={true}> - <OverviewHost - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: [...filters, ...filterHostData], - })} - startDate={from} - setQuery={setQuery} - /> - </EuiFlexItem> - - <HorizontalSpacer grow={false} /> - - <EuiFlexItem grow={true}> - <OverviewNetwork - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: [...filters, ...filterNetworkData], - })} - startDate={from} - setQuery={setQuery} - /> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx deleted file mode 100644 index 14cc29adb505a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Position } from '@elastic/charts'; -import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; -import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; -import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { - MatrixHisrogramConfigs, - MatrixHistogramOption, -} from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; -import { eventsStackByOptions } from '../../hosts/navigation'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { InputsModelId } from '../../../store/inputs/constants'; - -import * as i18n from '../translations'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.dataset'; - -const ID = 'eventsByDatasetOverview'; - -interface Props { - combinedQueries?: string; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - indexToAdd?: string[] | null; - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - showSpacer?: boolean; - to: number; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const EventsByDatasetComponent: React.FC<Props> = ({ - combinedQueries, - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - indexPattern, - indexToAdd, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePickerTarget, - setQuery, - showSpacer = true, - to, -}) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, [deleteQuery, uniqueQueryId]); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - - const eventsCountViewEventsButton = useMemo( - () => ( - <EuiButton href={getTabsOnHostsUrl(HostsTableType.events, urlSearch)}> - {i18n.VIEW_EVENTS} - </EuiButton> - ), - [urlSearch] - ); - - const filterQuery = useMemo( - () => - combinedQueries == null - ? convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }) - : combinedQueries, - [combinedQueries, kibana, indexPattern, query, filters] - ); - - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - stackByOptions: - onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, - defaultStackByOption: - onlyField != null - ? getHistogramOption(onlyField) - : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], - legendPosition: Position.Right, - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - titleSize: onlyField == null ? 'm' : 's', - }), - [onlyField, defaultNumberFormat] - ); - - const headerContent = useMemo(() => { - if (onlyField == null || headerChildren != null) { - return ( - <> - {headerChildren} - {onlyField == null && eventsCountViewEventsButton} - </> - ); - } else { - return null; - } - }, [onlyField, headerChildren, eventsCountViewEventsButton]); - - return ( - <MatrixHistogramContainer - endDate={to} - filterQuery={filterQuery} - headerChildren={headerContent} - id={uniqueQueryId} - indexToAdd={indexToAdd} - setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} - setQuery={setQuery} - showSpacer={showSpacer} - sourceId="default" - startDate={from} - type={HostsType.page} - {...eventsByDatasetHistogramConfigs} - title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} - /> - ); -}; - -EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; - -export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx deleted file mode 100644 index feba80539a11b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; - -import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; -import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config'; -import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; -import { Filter, IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { InputsModelId } from '../../../store/inputs/constants'; -import * as i18n from '../translations'; - -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; -const NO_FILTERS: Filter[] = []; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const SignalsByCategoryComponent: React.FC<Props> = ({ - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, - setAbsoluteRangeDatePickerTarget = 'global', - setQuery, - to, -}) => { - const { signalIndexName } = useSignalIndex(); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - const defaultStackByOption = - signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; - - return ( - <SignalsHistogramPanel - deleteQuery={deleteQuery} - defaultStackByOption={defaultStackByOption} - filters={filters} - from={from} - headerChildren={headerChildren} - onlyField={onlyField} - query={query} - signalIndexName={signalIndexName} - setQuery={setQuery} - showTotalSignalsCount={true} - showLinkToSignals={onlyField == null ? true : false} - stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} - legendPosition={'right'} - to={to} - title={i18n.SIGNAL_COUNT} - updateDateRange={updateDateRangeCallback} - /> - ); -}; - -SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; - -export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx deleted file mode 100644 index da4aad97e5b48..0000000000000 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - AppMountParameters, - CoreSetup, - CoreStart, - PluginInitializerContext, - Plugin as IPlugin, -} from '../../../../../src/core/public'; -import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; -import { Start as NewsfeedStart } from '../../../../../src/plugins/newsfeed/public'; -import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; -import { initTelemetry } from './lib/telemetry'; -import { KibanaServices } from './lib/kibana'; - -import { serviceNowActionType } from './lib/connectors'; - -import { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '../../../../plugins/triggers_actions_ui/public'; -import { SecurityPluginSetup } from '../../../../plugins/security/public'; - -export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; - -export interface SetupPlugins { - home: HomePublicPluginSetup; - security: SecurityPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; - usageCollection: UsageCollectionSetup; -} -export interface StartPlugins { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - inspector: InspectorStart; - newsfeed?: NewsfeedStart; - security: SecurityPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; - uiActions: UiActionsStart; -} -export type StartServices = CoreStart & StartPlugins; - -export type Setup = ReturnType<Plugin['setup']>; -export type Start = ReturnType<Plugin['start']>; - -export class Plugin implements IPlugin<Setup, Start> { - public id = 'siem'; - public name = 'SIEM'; - - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext - ) {} - - public setup(core: CoreSetup, plugins: SetupPlugins) { - initTelemetry(plugins.usageCollection, this.id); - - const security = plugins.security; - - core.application.register({ - id: this.id, - title: this.name, - async mount(context, params) { - const [coreStart, startPlugins] = await core.getStartServices(); - const { renderApp } = await import('./app'); - - plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); - return renderApp(coreStart, { ...startPlugins, security } as StartPlugins, params); - }, - }); - - return {}; - } - - public start(core: CoreStart, plugins: StartPlugins) { - KibanaServices.init({ ...core, ...plugins }); - - return {}; - } - - public stop() { - return {}; - } -} diff --git a/x-pack/legacy/plugins/siem/public/register_feature.ts b/x-pack/legacy/plugins/siem/public/register_feature.ts deleted file mode 100644 index b5e8f78ebc560..0000000000000 --- a/x-pack/legacy/plugins/siem/public/register_feature.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { APP_ID } from '../../../../plugins/siem/common/constants'; - -// TODO(rylnd): move this into Plugin.setup once we're on NP -npSetup.plugins.home.featureCatalogue.register({ - id: APP_ID, - title: 'SIEM', - description: 'Explore security metrics and logs for events and alerts', - icon: 'securityAnalyticsApp', - path: `/app/${APP_ID}`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -}); diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts deleted file mode 100644 index 0c0ac637a4229..0000000000000 --- 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 5b26957843f08..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -import { InspectQuery, Refetch, RefetchKql } from './model'; -import { InputsModelId } from './constants'; -import { Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); - -export const setAbsoluteRangeDatePicker = actionCreator<{ - id: InputsModelId; - from: number; - to: number; -}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); - -export const setTimelineRangeDatePicker = actionCreator<{ - from: number; - to: number; -}>('SET_TIMELINE_RANGE_DATE_PICKER'); - -export const setRelativeRangeDatePicker = actionCreator<{ - id: InputsModelId; - fromStr: string; - toStr: string; - from: number; - to: number; -}>('SET_RELATIVE_RANGE_DATE_PICKER'); - -export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); - -export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); - -export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); - -export const setQuery = actionCreator<{ - inputId: InputsModelId; - id: string; - loading: boolean; - refetch: Refetch | RefetchKql; - inspect: InspectQuery | null; -}>('SET_QUERY'); - -export const deleteOneQuery = actionCreator<{ - inputId: InputsModelId; - id: string; -}>('DELETE_QUERY'); - -export const setInspectionParameter = actionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; -}>('SET_INSPECTION_PARAMETER'); - -export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); - -export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( - 'TOGGLE_TIMELINE_LINK_TO' -); - -export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); -export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); - -export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); -export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); - -export const setFilterQuery = actionCreator<{ - id: InputsModelId; - query: string | { [key: string]: unknown }; - language: string; -}>('SET_FILTER_QUERY'); - -export const setSavedQuery = actionCreator<{ - id: InputsModelId; - savedQuery: SavedQuery | undefined; -}>('SET_SAVED_QUERY'); - -export const setSearchBarFilter = actionCreator<{ - id: InputsModelId; - filters: Filter[]; -}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts deleted file mode 100644 index e851caf523eb4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Dispatch } from 'redux'; -import { InputsModelId } from './constants'; -import { CONSTANTS } from '../../components/url_state/constants'; -import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; - -export interface AbsoluteTimeRange { - kind: 'absolute'; - fromStr: undefined; - toStr: undefined; - from: number; - to: number; -} - -export interface RelativeTimeRange { - kind: 'relative'; - fromStr: string; - toStr: string; - from: number; - to: number; -} - -export const isRelativeTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; - -export const isAbsoluteTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; - -export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; - -export type URLTimeRange = Omit<TimeRange, 'from' | 'to'> & { - from: string | TimeRange['from']; - to: string | TimeRange['to']; -}; - -export interface Policy { - kind: 'manual' | 'interval'; - duration: number; // in ms -} - -interface InspectVariables { - inspect: boolean; -} -export type RefetchWithParams = ({ inspect }: InspectVariables) => void; -export type RefetchKql = (dispatch: Dispatch) => boolean; -export type Refetch = () => void; - -export interface InspectQuery { - dsl: string[]; - response: string[]; -} - -export interface GlobalGenericQuery { - inspect: InspectQuery | null; - isInspected: boolean; - loading: boolean; - selectedInspectIndex: number; -} - -export interface GlobalGraphqlQuery extends GlobalGenericQuery { - id: string; - refetch: null | Refetch | RefetchWithParams; -} -export interface GlobalKqlQuery extends GlobalGenericQuery { - id: 'kql'; - refetch: RefetchKql; -} - -export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; - -export interface InputsRange { - timerange: TimeRange; - policy: Policy; - queries: GlobalQuery[]; - linkTo: InputsModelId[]; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; -} - -export interface LinkTo { - linkTo: InputsModelId[]; -} - -export interface InputsModel { - global: InputsRange; - timeline: InputsRange; -} -export interface UrlInputsModelInputs { - linkTo: InputsModelId[]; - [CONSTANTS.timerange]: TimeRange; -} -export interface UrlInputsModel { - global: UrlInputsModelInputs; - timeline: UrlInputsModelInputs; -} diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts deleted file mode 100644 index 9e9e663a59fe0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { appModel } from './app'; -export { dragAndDropModel } from './drag_and_drop'; -export { hostsModel } from './hosts'; -export { inputsModel } from './inputs'; -export { networkModel } from './network'; - -export type KueryFilterQueryKind = 'kuery' | 'lucene'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts deleted file mode 100644 index 51043b999c27e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); - -export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); - -export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); - -export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( - 'ADD_NOTE_TO_EVENT' -); - -export const upsertColumn = actionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>('UPSERT_COLUMN'); - -export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); - -export const applyDeltaToWidth = actionCreator<{ - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; -}>('APPLY_DELTA_TO_WIDTH'); - -export const applyDeltaToColumnWidth = actionCreator<{ - id: string; - columnId: string; - delta: number; -}>('APPLY_DELTA_TO_COLUMN_WIDTH'); - -export const createTimeline = actionCreator<{ - id: string; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - columns: ColumnHeaderOptions[]; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; -}>('CREATE_TIMELINE'); - -export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); - -export const removeColumn = actionCreator<{ - id: string; - columnId: string; -}>('REMOVE_COLUMN'); - -export const removeProvider = actionCreator<{ - id: string; - providerId: string; - andProviderId?: string; -}>('REMOVE_PROVIDER'); - -export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); - -export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); - -export const updateTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('UPDATE_TIMELINE'); - -export const addTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('ADD_TIMELINE'); - -export const startTimelineSaving = actionCreator<{ - id: string; -}>('START_TIMELINE_SAVING'); - -export const endTimelineSaving = actionCreator<{ - id: string; -}>('END_TIMELINE_SAVING'); - -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - -export const updateColumns = actionCreator<{ - id: string; - columns: ColumnHeaderOptions[]; -}>('UPDATE_COLUMNS'); - -export const updateDataProviderEnabled = actionCreator<{ - id: string; - enabled: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_ENABLED'); - -export const updateDataProviderExcluded = actionCreator<{ - id: string; - excluded: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_EXCLUDED'); - -export const dataProviderEdited = actionCreator<{ - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - value: string | number; -}>('DATA_PROVIDER_EDITED'); - -export const updateDataProviderKqlQuery = actionCreator<{ - id: string; - kqlQuery: string; - providerId: string; -}>('PROVIDER_EDIT_KQL_QUERY'); - -export const updateHighlightedDropAndProviderId = actionCreator<{ - id: string; - providerId: string; -}>('UPDATE_DROP_AND_PROVIDER'); - -export const updateDescription = actionCreator<{ id: string; description: string }>( - 'UPDATE_DESCRIPTION' -); - -export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); - -export const setKqlFilterQueryDraft = actionCreator<{ - id: string; - filterQueryDraft: KueryFilterQuery; -}>('SET_KQL_FILTER_QUERY_DRAFT'); - -export const applyKqlFilterQuery = actionCreator<{ - id: string; - filterQuery: SerializedFilterQuery; -}>('APPLY_KQL_FILTER_QUERY'); - -export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( - 'UPDATE_IS_FAVORITE' -); - -export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); - -export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( - 'UPDATE_ITEMS_PER_PAGE' -); - -export const updateItemsPerPageOptions = actionCreator<{ - id: string; - itemsPerPageOptions: number[]; -}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); - -export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); - -export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( - 'UPDATE_PAGE_INDEX' -); - -export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( - 'UPDATE_PROVIDERS' -); - -export const updateRange = actionCreator<{ id: string; start: number; end: number }>( - 'UPDATE_RANGE' -); - -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); - -export const updateAutoSaveMsg = actionCreator<{ - timelineId: string | null; - newTimelineModel: TimelineModel | null; -}>('UPDATE_AUTO_SAVE'); - -export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); - -export const setSavedQueryId = actionCreator<{ - id: string; - savedQueryId: string | null; -}>('SET_TIMELINE_SAVED_QUERY'); - -export const setFilters = actionCreator<{ - id: string; - filters: Filter[]; -}>('SET_TIMELINE_FILTERS'); - -export const setSelected = actionCreator<{ - id: string; - eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - isSelected: boolean; - isSelectAllChecked: boolean; -}>('SET_TIMELINE_SELECTED'); - -export const clearSelected = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_SELECTED'); - -export const setEventsLoading = actionCreator<{ - id: string; - eventIds: string[]; - isLoading: boolean; -}>('SET_TIMELINE_EVENTS_LOADING'); - -export const clearEventsLoading = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_LOADING'); - -export const setEventsDeleted = actionCreator<{ - id: string; - eventIds: string[]; - isDeleted: boolean; -}>('SET_TIMELINE_EVENTS_DELETED'); - -export const clearEventsDeleted = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_DELETED'); - -export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( - 'UPDATE_EVENT_TYPE' -); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts deleted file mode 100644 index fa70c1b04608d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ /dev/null @@ -1,1324 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, - QueryMatch, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -import { timelineDefaults } from './defaults'; -import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; -import { TimelineById, TimelineState } from './types'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference - -export const isNotNull = <T>(value: T | null): value is T => value !== null; - -export const initialTimelineState: TimelineState = { - timelineById: EMPTY_TIMELINE_BY_ID, - autoSavedWarningMsg: { - timelineId: null, - newTimelineModel: null, - }, - showCallOutUnauthorizedMsg: false, -}; - -interface AddTimelineHistoryParams { - id: string; - historyId: string; - timelineById: TimelineById; -} - -export const addTimelineHistory = ({ - id, - historyId, - timelineById, -}: AddTimelineHistoryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - historyIds: uniq([...timeline.historyIds, historyId]), - }, - }; -}; - -interface AddTimelineNoteParams { - id: string; - noteId: string; - timelineById: TimelineById; -} - -export const addTimelineNote = ({ - id, - noteId, - timelineById, -}: AddTimelineNoteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - noteIds: [...timeline.noteIds, noteId], - }, - }; -}; - -interface AddTimelineNoteToEventParams { - id: string; - noteId: string; - eventId: string; - timelineById: TimelineById; -} - -export const addTimelineNoteToEvent = ({ - id, - noteId, - eventId, - timelineById, -}: AddTimelineNoteToEventParams): TimelineById => { - const timeline = timelineById[id]; - const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); - - return { - ...timelineById, - [id]: { - ...timeline, - eventIdToNoteIds: { - ...timeline.eventIdToNoteIds, - ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, - }, - }, - }; -}; - -interface AddTimelineParams { - id: string; - timeline: TimelineModel; - timelineById: TimelineById; -} - -/** - * Add a saved object timeline to the store - * and default the value to what need to be if values are null - */ -export const addTimelineToStore = ({ - id, - timeline, - timelineById, -}: AddTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - ...timeline, - isLoading: timelineById[id].isLoading, - }, -}); - -interface AddNewTimelineParams { - columns: ColumnHeaderOptions[]; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - id: string; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; - timelineById: TimelineById; -} - -/** Adds a new `Timeline` to the provided collection of `TimelineById` */ -export const addNewTimeline = ({ - columns, - dataProviders = [], - dateRange = { start: 0, end: 0 }, - filters = timelineDefaults.filters, - id, - itemsPerPage = timelineDefaults.itemsPerPage, - kqlQuery = { filterQuery: null, filterQueryDraft: null }, - sort = timelineDefaults.sort, - show = false, - showCheckboxes = false, - showRowRenderers = true, - timelineById, -}: AddNewTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - id, - ...timelineDefaults, - columns, - dataProviders, - dateRange, - filters, - itemsPerPage, - kqlQuery, - sort, - show, - savedObjectId: null, - version: null, - isSaving: false, - isLoading: false, - showCheckboxes, - showRowRenderers, - }, -}); - -interface PinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const pinTimelineEvent = ({ - id, - eventId, - timelineById, -}: PinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: { - ...timeline.pinnedEventIds, - ...{ [eventId]: true }, - }, - }, - }; -}; - -interface UpdateShowTimelineProps { - id: string; - show: boolean; - timelineById: TimelineById; -} - -export const updateTimelineShowTimeline = ({ - id, - show, - timelineById, -}: UpdateShowTimelineProps): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - show, - }, - }; -}; - -interface ApplyDeltaToCurrentWidthParams { - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; - timelineById: TimelineById; -} - -export const applyDeltaToCurrentWidth = ({ - id, - delta, - bodyClientWidthPixels, - minWidthPixels, - maxWidthPercent, - timelineById, -}: ApplyDeltaToCurrentWidthParams): TimelineById => { - const timeline = timelineById[id]; - - const requestedWidth = timeline.width + delta * -1; // raw change in width - const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; - const clampedWidth = Math.min(requestedWidth, maxWidthPixels); - const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min - - return { - ...timelineById, - [id]: { - ...timeline, - width, - }, - }; -}; - -const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { - if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { - return true; - } - return false; -}; - -const addAndToProviderInTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( - p => p.id === timeline.highlightedDropAndProviderId - ); - const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; - const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); - const { and, ...andProvider } = provider; - - if ( - isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || - (alreadyExistsAndProviderIndex === -1 && - newProvider.and.filter(itemAndProvider => - isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) - ).length > 0) - ) { - return timelineById; - } - - const dataProviders = [ - ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), - { - ...timeline.dataProviders[alreadyExistsProviderIndex], - and: - alreadyExistsAndProviderIndex > -1 - ? [ - ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), - andProvider, - ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), - ] - : [...newProvider.and, andProvider], - }, - ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -const addProviderToTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); - - if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { - provider.id = `${provider.id}-${ - timeline.dataProviders.filter(p => p.id === provider.id).length - }`; - } - - const dataProviders = - alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) - ? [ - ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), - provider, - ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), - ] - : [...timeline.dataProviders, provider]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -interface AddTimelineColumnParams { - column: ColumnHeaderOptions; - id: string; - index: number; - timelineById: TimelineById; -} - -/** - * Adds or updates a column. When updating a column, it will be moved to the - * new index - */ -export const upsertTimelineColumn = ({ - column, - id, - index, - timelineById, -}: AddTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); - - if (alreadyExistsAtIndex !== -1) { - // remove the existing entry and add the new one at the specified index - const reordered = timeline.columns.filter(c => c.id !== column.id); - reordered.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns: reordered, - }, - }; - } - - // add the new entry at the specified index - const columns = [...timeline.columns]; - columns.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface RemoveTimelineColumnParams { - id: string; - columnId: string; - timelineById: TimelineById; -} - -export const removeTimelineColumn = ({ - id, - columnId, - timelineById, -}: RemoveTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - - const columns = timeline.columns.filter(c => c.id !== columnId); - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface ApplyDeltaToTimelineColumnWidth { - id: string; - columnId: string; - delta: number; - timelineById: TimelineById; -} - -export const applyDeltaToTimelineColumnWidth = ({ - id, - columnId, - delta, - timelineById, -}: ApplyDeltaToTimelineColumnWidth): TimelineById => { - const timeline = timelineById[id]; - - const columnIndex = timeline.columns.findIndex(c => c.id === columnId); - if (columnIndex === -1) { - // the column was not found - return { - ...timelineById, - [id]: { - ...timeline, - }, - }; - } - const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); - const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width - const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min - - const columnWithNewWidth = { - ...timeline.columns[columnIndex], - width, - }; - - const columns = [ - ...timeline.columns.slice(0, columnIndex), - columnWithNewWidth, - ...timeline.columns.slice(columnIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface AddTimelineProviderParams { - id: string; - provider: DataProvider; - timelineById: TimelineById; -} - -export const addTimelineProvider = ({ - id, - provider, - timelineById, -}: AddTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - if (timeline.highlightedDropAndProviderId !== '') { - return addAndToProviderInTimeline(id, provider, timeline, timelineById); - } else { - return addProviderToTimeline(id, provider, timeline, timelineById); - } -}; - -interface ApplyKqlFilterQueryDraftParams { - id: string; - filterQuery: SerializedFilterQuery; - timelineById: TimelineById; -} - -export const applyKqlFilterQueryDraft = ({ - id, - filterQuery, - timelineById, -}: ApplyKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQuery, - }, - }, - }; -}; - -interface UpdateTimelineKqlModeParams { - id: string; - kqlMode: KqlMode; - timelineById: TimelineById; -} - -export const updateTimelineKqlMode = ({ - id, - kqlMode, - timelineById, -}: UpdateTimelineKqlModeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlMode, - }, - }; -}; - -interface UpdateKqlFilterQueryDraftParams { - id: string; - filterQueryDraft: KueryFilterQuery; - timelineById: TimelineById; -} - -export const updateKqlFilterQueryDraft = ({ - id, - filterQueryDraft, - timelineById, -}: UpdateKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQueryDraft, - }, - }, - }; -}; - -interface UpdateTimelineColumnsParams { - id: string; - columns: ColumnHeaderOptions[]; - timelineById: TimelineById; -} - -export const updateTimelineColumns = ({ - id, - columns, - timelineById, -}: UpdateTimelineColumnsParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface UpdateTimelineDescriptionParams { - id: string; - description: string; - timelineById: TimelineById; -} - -export const updateTimelineDescription = ({ - id, - description, - timelineById, -}: UpdateTimelineDescriptionParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), - }, - }; -}; - -interface UpdateTimelineTitleParams { - id: string; - title: string; - timelineById: TimelineById; -} - -export const updateTimelineTitle = ({ - id, - title, - timelineById, -}: UpdateTimelineTitleParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), - }, - }; -}; - -interface UpdateTimelineEventTypeParams { - id: string; - eventType: EventType; - timelineById: TimelineById; -} - -export const updateTimelineEventType = ({ - id, - eventType, - timelineById, -}: UpdateTimelineEventTypeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - eventType, - }, - }; -}; - -interface UpdateTimelineIsFavoriteParams { - id: string; - isFavorite: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsFavorite = ({ - id, - isFavorite, - timelineById, -}: UpdateTimelineIsFavoriteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isFavorite, - }, - }; -}; - -interface UpdateTimelineIsLiveParams { - id: string; - isLive: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsLive = ({ - id, - isLive, - timelineById, -}: UpdateTimelineIsLiveParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isLive, - }, - }; -}; - -interface UpdateTimelineProvidersParams { - id: string; - providers: DataProvider[]; - timelineById: TimelineById; -} - -export const updateTimelineProviders = ({ - id, - providers, - timelineById, -}: UpdateTimelineProvidersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: providers, - }, - }; -}; - -interface UpdateTimelineRangeParams { - id: string; - start: number; - end: number; - timelineById: TimelineById; -} - -export const updateTimelineRange = ({ - id, - start, - end, - timelineById, -}: UpdateTimelineRangeParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dateRange: { - start, - end, - }, - }, - }; -}; - -interface UpdateTimelineSortParams { - id: string; - sort: Sort; - timelineById: TimelineById; -} - -export const updateTimelineSort = ({ - id, - sort, - timelineById, -}: UpdateTimelineSortParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - sort, - }, - }; -}; - -const updateEnabledAndProvider = ( - andProviderId: string, - enabled: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider - ), - } - : provider - ); - -const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - enabled, - } - : provider - ); - -interface UpdateTimelineProviderEnabledParams { - id: string; - providerId: string; - enabled: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderEnabled = ({ - id, - providerId, - enabled, - timelineById, - andProviderId, -}: UpdateTimelineProviderEnabledParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) - : updateEnabledProvider(enabled, providerId, timeline), - }, - }; -}; - -const updateExcludedAndProvider = ( - andProviderId: string, - excluded: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider - ), - } - : provider - ); - -const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - } - : provider - ); - -interface UpdateTimelineProviderExcludedParams { - id: string; - providerId: string; - excluded: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderExcluded = ({ - id, - providerId, - excluded, - timelineById, - andProviderId, -}: UpdateTimelineProviderExcludedParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) - : updateExcludedProvider(excluded, providerId, timeline), - }, - }; -}; - -const updateProviderProperties = ({ - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - queryMatch: { - ...provider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : provider - ); - -const updateAndProviderProperties = ({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - andProviderId: string; - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId - ? { - ...andProvider, - excluded, - queryMatch: { - ...andProvider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : andProvider - ), - } - : provider - ); - -interface UpdateTimelineProviderEditPropertiesParams { - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - timelineById: TimelineById; - value: string | number; -} - -export const updateTimelineProviderProperties = ({ - andProviderId, - excluded, - field, - id, - operator, - providerId, - timelineById, - value, -}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateAndProviderProperties({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, - }) - : updateProviderProperties({ - excluded, - field, - operator, - providerId, - timeline, - value, - }), - }, - }; -}; - -interface UpdateTimelineProviderKqlQueryParams { - id: string; - providerId: string; - kqlQuery: string; - timelineById: TimelineById; -} - -export const updateTimelineProviderKqlQuery = ({ - id, - providerId, - kqlQuery, - timelineById, -}: UpdateTimelineProviderKqlQueryParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: timeline.dataProviders.map(provider => - provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider - ), - }, - }; -}; - -interface UpdateTimelineItemsPerPageParams { - id: string; - itemsPerPage: number; - timelineById: TimelineById; -} - -export const updateTimelineItemsPerPage = ({ - id, - itemsPerPage, - timelineById, -}: UpdateTimelineItemsPerPageParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPage, - }, - }; -}; - -interface UpdateTimelinePageIndexParams { - id: string; - activePage: number; - timelineById: TimelineById; -} - -export const updateTimelinePageIndex = ({ - id, - activePage, - timelineById, -}: UpdateTimelinePageIndexParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - activePage, - }, - }; -}; - -interface UpdateTimelinePerPageOptionsParams { - id: string; - itemsPerPageOptions: number[]; - timelineById: TimelineById; -} - -export const updateTimelinePerPageOptions = ({ - id, - itemsPerPageOptions, - timelineById, -}: UpdateTimelinePerPageOptionsParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPageOptions, - }, - }; -}; - -const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( - p => p.id === andProviderId - ); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - { - ...timeline.dataProviders[providerIndex], - and: [ - ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), - ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), - ], - }, - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -const removeProvider = (providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - ...(timeline.dataProviders[providerIndex].and.length - ? [ - { - ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], - and: [...timeline.dataProviders[providerIndex].and.slice(1)], - }, - ] - : []), - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -interface RemoveTimelineProviderParams { - id: string; - providerId: string; - timelineById: TimelineById; - andProviderId?: string; -} - -export const removeTimelineProvider = ({ - id, - providerId, - timelineById, - andProviderId, -}: RemoveTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? removeAndProvider(andProviderId, providerId, timeline) - : removeProvider(providerId, timeline), - }, - }; -}; - -interface SetDeletedTimelineEventsParams { - id: string; - eventIds: string[]; - isDeleted: boolean; - timelineById: TimelineById; -} - -export const setDeletedTimelineEvents = ({ - id, - eventIds, - isDeleted, - timelineById, -}: SetDeletedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const deletedEventIds = isDeleted - ? union(timeline.deletedEventIds, eventIds) - : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - const selectedEventIds = Object.fromEntries( - Object.entries(timeline.selectedEventIds).filter( - ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) - ) - ); - - const isSelectAllChecked = - Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; - - return { - ...timelineById, - [id]: { - ...timeline, - deletedEventIds, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface SetLoadingTimelineEventsParams { - id: string; - eventIds: string[]; - isLoading: boolean; - timelineById: TimelineById; -} - -export const setLoadingTimelineEvents = ({ - id, - eventIds, - isLoading, - timelineById, -}: SetLoadingTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const loadingEventIds = isLoading - ? union(timeline.loadingEventIds, eventIds) - : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - return { - ...timelineById, - [id]: { - ...timeline, - loadingEventIds, - }, - }; -}; - -interface SetSelectedTimelineEventsParams { - id: string; - eventIds: Record<string, TimelineNonEcsData[]>; - isSelectAllChecked: boolean; - isSelected: boolean; - timelineById: TimelineById; -} - -export const setSelectedTimelineEvents = ({ - id, - eventIds, - isSelectAllChecked = false, - isSelected, - timelineById, -}: SetSelectedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const selectedEventIds = isSelected - ? { ...timeline.selectedEventIds, ...eventIds } - : omit(Object.keys(eventIds), timeline.selectedEventIds); - - return { - ...timelineById, - [id]: { - ...timeline, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface UnPinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const unPinTimelineEvent = ({ - id, - eventId, - timelineById, -}: UnPinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: omit(eventId, timeline.pinnedEventIds), - }, - }; -}; - -interface UpdateHighlightedDropAndProviderIdParams { - id: string; - providerId: string; - timelineById: TimelineById; -} - -export const updateHighlightedDropAndProvider = ({ - id, - providerId, - timelineById, -}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - highlightedDropAndProviderId: providerId, - }, - }; -}; - -interface UpdateSavedQueryParams { - id: string; - savedQueryId: string | null; - timelineById: TimelineById; -} - -export const updateSavedQuery = ({ - id, - savedQueryId, - timelineById, -}: UpdateSavedQueryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - savedQueryId, - }, - }; -}; - -interface UpdateFiltersParams { - id: string; - filters: Filter[]; - timelineById: TimelineById; -} - -export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - filters, - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts deleted file mode 100644 index ef46a8d061c2e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; -import { Sort } from '../../components/timeline/body/sort'; -import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages -export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'signal'; - -export type ColumnHeaderType = 'not-filtered' | 'text-filter'; - -/** Uniquely identifies a column */ -export type ColumnId = string; - -/** The specification of a column header */ -export interface ColumnHeaderOptions { - aggregatable?: boolean; - category?: string; - columnHeaderType: ColumnHeaderType; - description?: string; - example?: string; - format?: string; - id: ColumnId; - label?: string; - linkField?: string; - placeholder?: string; - type?: string; - width: number; -} - -export interface TimelineModel { - /** The columns displayed in the timeline */ - columns: ColumnHeaderOptions[]; - /** The sources of the event data shown in the timeline */ - dataProviders: DataProvider[]; - /** Events to not be rendered **/ - deletedEventIds: string[]; - /** A summary of the events and notes in this timeline */ - description: string; - /** Typoe of event you want to see in this timeline */ - eventType?: EventType; - /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ - eventIdToNoteIds: Record<string, string[]>; - filters?: Filter[]; - /** The chronological history of actions related to this timeline */ - historyIds: string[]; - /** The chronological history of actions related to this timeline */ - highlightedDropAndProviderId: string; - /** Uniquely identifies the timeline */ - id: string; - /** If selectAll checkbox in header is checked **/ - isSelectAllChecked: boolean; - /** Events to be rendered as loading **/ - loadingEventIds: string[]; - savedObjectId: string | null; - /** When true, this timeline was marked as "favorite" by the user */ - isFavorite: boolean; - /** When true, the timeline will update as new data arrives */ - isLive: boolean; - /** The number of items to show in a single page of results */ - itemsPerPage: number; - /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ - itemsPerPageOptions: number[]; - /** determines the behavior of the KQL bar */ - kqlMode: KqlMode; - /** the KQL query in the KQL bar */ - kqlQuery: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - /** Title */ - title: string; - /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ - noteIds: string[]; - /** Events pinned to this timeline */ - pinnedEventIds: Record<string, boolean>; - pinnedEventsSaveObject: Record<string, PinnedEvent>; - /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ - dateRange: { - start: number; - end: number; - }; - savedQueryId?: string | null; - /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ - selectedEventIds: Record<string, TimelineNonEcsData[]>; - /** When true, show the timeline flyover */ - show: boolean; - /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ - showCheckboxes: boolean; - /** When true, shows additional rowRenderers below the PlainRowRenderer **/ - showRowRenderers: boolean; - /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; - /** Persists the UI state (width) of the timeline flyover */ - width: number; - /** timeline is saving */ - isSaving: boolean; - isLoading: boolean; - version: string | null; -} - -export type SubsetTimelineModel = Readonly< - Pick< - TimelineModel, - | 'columns' - | 'dataProviders' - | 'deletedEventIds' - | 'description' - | 'eventType' - | 'eventIdToNoteIds' - | 'highlightedDropAndProviderId' - | 'historyIds' - | 'isFavorite' - | 'isLive' - | 'isSelectAllChecked' - | 'itemsPerPage' - | 'itemsPerPageOptions' - | 'kqlMode' - | 'kqlQuery' - | 'title' - | 'loadingEventIds' - | 'noteIds' - | 'pinnedEventIds' - | 'pinnedEventsSaveObject' - | 'dateRange' - | 'selectedEventIds' - | 'show' - | 'showCheckboxes' - | 'showRowRenderers' - | 'sort' - | 'width' - | 'isSaving' - | 'isLoading' - | 'savedObjectId' - | 'version' - > ->; - -export interface TimelineUrl { - id: string; - isOpen: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx deleted file mode 100644 index a8ee10ba2b801..0000000000000 --- a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useState, useEffect } from 'react'; -import { - SavedQueryService, - createSavedQueryService, -} from '../../../../../../../src/plugins/data/public'; - -import { useKibana } from '../../lib/kibana'; - -export const useSavedQueryServices = () => { - const kibana = useKibana(); - const client = kibana.services.savedObjects.client; - - const [savedQueryService, setSavedQueryService] = useState<SavedQueryService>( - createSavedQueryService(client) - ); - - useEffect(() => { - setSavedQueryService(createSavedQueryService(client)); - }, [client]); - return savedQueryService; -}; diff --git a/x-pack/legacy/plugins/siem/yarn.lock b/x-pack/legacy/plugins/siem/yarn.lock deleted file mode 120000 index 4b16253de2abe..0000000000000 --- a/x-pack/legacy/plugins/siem/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../../../yarn.lock \ No newline at end of file diff --git a/x-pack/legacy/plugins/task_manager/server/index.ts b/x-pack/legacy/plugins/task_manager/server/index.ts index 3ea687f7003f4..a3167920efa06 100644 --- a/x-pack/legacy/plugins/task_manager/server/index.ts +++ b/x-pack/legacy/plugins/task_manager/server/index.ts @@ -6,8 +6,6 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import mappings from './mappings.json'; -import { migrations } from './migrations'; import { createLegacyApi, getTaskManagerSetup } from './legacy'; export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from './legacy'; @@ -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 eb74290c84682..0000000000000 --- a/x-pack/legacy/plugins/triggers_actions_ui/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { Root } from 'joi'; -import { resolve } from 'path'; - -export function triggersActionsUI(kibana: any) { - return new kibana.Plugin({ - id: 'triggers_actions_ui', - configPrefix: 'xpack.triggers_actions_ui', - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.triggers_actions_ui.enabled') && - (config.get('xpack.actions.enabled') || config.get('xpack.alerting.enabled')) - ); - }, - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - config(Joi: Root) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - }, - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); -} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss b/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss deleted file mode 100644 index 8c83c0a571f28..0000000000000 --- a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -// Imported EUI -@import 'src/legacy/ui/public/styles/_styling_constants'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts deleted file mode 100644 index b5e8ce4750215..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Legacy } from 'kibana'; -import mappings from './mappings.json'; - -export function upgradeAssistant(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'upgrade_assistant', - uiExports: { - // @ts-ignore - savedObjectSchemas: { - 'upgrade-assistant-reindex-operation': { - isNamespaceAgnostic: true, - }, - 'upgrade-assistant-telemetry': { - isNamespaceAgnostic: true, - }, - }, - mappings, - }, - - init() {}, - }; - return new kibana.Plugin(config); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/mappings.json b/x-pack/legacy/plugins/upgrade_assistant/mappings.json deleted file mode 100644 index 885ac4d5a9b44..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/mappings.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "upgrade-assistant-reindex-operation": { - "dynamic": true, - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "ui_open": { - "properties": { - "overview": { - "type": "long", - "null_value": 0 - }, - "cluster": { - "type": "long", - "null_value": 0 - }, - "indices": { - "type": "long", - "null_value": 0 - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long", - "null_value": 0 - }, - "open": { - "type": "long", - "null_value": 0 - }, - "start": { - "type": "long", - "null_value": 0 - }, - "stop": { - "type": "long", - "null_value": 0 - } - } - }, - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "type": "boolean", - "null_value": true - } - } - } - } - } - } - } -} diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts deleted file mode 100644 index 00781726941d5..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PLUGIN = { - APP_ROOT_ID: 'react-uptime-root', - DESCRIPTION: 'Uptime monitoring', - ID: 'uptime', - LOCAL_STORAGE_KEY: 'xpack.uptime', - NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { - defaultMessage: 'Uptime', - }), - ROUTER_BASE_NAME: '/app/uptime#', - TITLE: 'uptime', -}; diff --git a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts b/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts deleted file mode 100644 index 22df610d2d516..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -type UMResolverResult<T> = Promise<T> | T; - -export type UMResolver<Result, Parent, Args, Context> = ( - parent: Parent, - args: Args, - context: Context -) => UMResolverResult<Result>; diff --git a/x-pack/legacy/plugins/uptime/common/graphql/types.ts b/x-pack/legacy/plugins/uptime/common/graphql/types.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts deleted file mode 100644 index 78aab3806ae04..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './alerts'; -export * from './certs'; -export * from './common'; -export * from './monitor'; -export * from './overview_filters'; -export * from './ping'; -export * from './snapshot'; -export * from './dynamic_settings'; diff --git a/x-pack/legacy/plugins/uptime/common/types/index.ts b/x-pack/legacy/plugins/uptime/common/types/index.ts deleted file mode 100644 index a32eabd49a3e5..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/types/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** Represents a bucket of monitor status information. */ -export interface StatusData { - /** The timeseries point for this status data. */ - x: number; - /** The value of up counts for this point. */ - up?: number | null; - /** The value for down counts for this point. */ - down?: number | null; - /** The total down counts for this point. */ - total?: number | null; -} - -/** Represents the average monitor duration ms at a point in time. */ -export interface MonitorDurationAveragePoint { - /** The timeseries value for this point. */ - x: number; - /** The average duration ms for the monitor. */ - y?: number | null; -} - -export interface LocationDurationLine { - name: string; - - line: MonitorDurationAveragePoint[]; -} - -/** The data used to populate the monitor charts. */ -export interface MonitorDurationResult { - /** The average values for the monitor duration. */ - locationDurationLines: LocationDurationLine[]; -} - -export interface MonitorIdParam { - monitorId: string; -} diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts deleted file mode 100644 index f52ad8ce867b6..0000000000000 --- a/x-pack/legacy/plugins/uptime/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; - -export const uptime = (kibana: any) => - new kibana.Plugin({ - configPrefix: 'xpack.uptime', - id: PLUGIN.ID, - publicDir: resolve(__dirname, 'public'), - require: ['alerting', 'kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - app: { - description: i18n.translate('xpack.uptime.pluginDescription', { - defaultMessage: 'Uptime monitoring', - description: 'The description text that will be shown to users in Kibana', - }), - icon: 'plugins/uptime/icons/heartbeat_white.svg', - euiIconType: 'uptimeApp', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { - defaultMessage: 'Uptime', - }), - main: 'plugins/uptime/app', - order: 8900, - url: '/app/uptime#/', - category: DEFAULT_APP_CATEGORIES.observability, - }, - home: ['plugins/uptime/register_feature'], - }, - }); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts deleted file mode 100644 index d58bf8398fcde..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { Plugin } from './plugin'; -import 'uiExports/embeddableFactories'; - -const plugin = new Plugin({ - opaqueId: Symbol('uptime'), - env: {} as any, - config: { get: () => ({} as any) }, -}); -plugin.setup(npSetup); diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts deleted file mode 100644 index e73598c44c9f0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LegacyCoreSetup, PluginInitializerContext, AppMountParameters } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; -import { UMFrontendLibs } from '../lib/lib'; -import { PLUGIN } from '../../common/constants'; -import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; - -export interface SetupObject { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -export class Plugin { - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext - ) {} - - public setup(setup: SetupObject) { - const { core, plugins } = setup; - const { home } = plugins; - - home.featureCatalogue.register({ - category: FeatureCatalogueCategory.DATA, - description: PLUGIN.DESCRIPTION, - icon: 'uptimeApp', - id: PLUGIN.ID, - path: '/app/uptime#/', - showOnHomePage: true, - title: PLUGIN.TITLE, - }); - - core.application.register({ - id: PLUGIN.ID, - euiIconType: 'uptimeApp', - order: 8900, - title: 'Uptime', - async mount(params: AppMountParameters) { - const [coreStart] = await core.getStartServices(); - const { element } = params; - const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(coreStart, plugins), - }; - libs.framework.render(element); - return () => {}; - }, - }); - } -} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx deleted file mode 100644 index 85d0b1b593704..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect, useState, useContext, useRef } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; - -import { - ViewMode, - EmbeddableOutput, - ErrorEmbeddable, - isErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; -import * as i18n from './translations'; -import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public'; -import { Location } from '../../../../../common/runtime_types'; - -import { getLayerList } from './map_config'; -import { UptimeThemeContext } from '../../../../contexts'; - -export interface EmbeddedMapProps { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export type LocationPoint = Required<Location>; - -const EmbeddedPanel = styled.div` - z-index: auto; - flex: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - .embPanel__content { - display: flex; - flex: 1 1 100%; - z-index: 1; - min-height: 0; // Absolute must for Firefox to scroll contents - } - &&& .mapboxgl-canvas { - animation: none !important; - } -`; - -export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { - const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState<MapEmbeddable | ErrorEmbeddable | undefined>(); - const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null); - const factory = npStart.plugins.embeddable.getEmbeddableFactory< - MapEmbeddableInput, - EmbeddableOutput, - MapEmbeddable - >(MAP_SAVED_OBJECT_TYPE); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It wil also omit Greenland/Antarctica etc - mapCenter: { - lon: 11, - lat: 20, - zoom: 0, - }, - disableInteractive: true, - disableTooltipControl: true, - hideToolbarOverlay: true, - hideLayerControl: true, - hideViewControl: true, - }; - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject = await factory.create({ - ...input, - title: i18n.MAP_TITLE, - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - - setEmbeddable(embeddableObject); - } - setupEmbeddable(); - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update map layers based on points - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - }, [upPoints, downPoints, embeddable, colors]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot]); - - return ( - <EmbeddedPanel> - <div - data-test-subj="xpack.uptime.locationMap.embeddedPanel" - className="embPanel__content" - ref={embeddableRoot} - /> - </EmbeddedPanel> - ); -}); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/index.ts b/x-pack/legacy/plugins/uptime/public/contexts/index.ts deleted file mode 100644 index 2b27fcfe907ab..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/contexts/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { UptimeRefreshContext, UptimeRefreshContextProvider } from './uptime_refresh_context'; -export { - UptimeSettingsContextValues, - UptimeSettingsContext, - UptimeSettingsContextProvider, -} from './uptime_settings_context'; -export { UptimeThemeContextProvider, UptimeThemeContext } from './uptime_theme_context'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts deleted file mode 100644 index 74160577cb0b1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// TODO: after NP migration is complete we should be able to remove this lint ignore comment -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../../plugins/triggers_actions_ui/public'; -import { initMonitorStatusAlertType } from './monitor_status'; - -export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; - -export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx deleted file mode 100644 index d8c2a78092854..0000000000000 --- 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 2f83fa33ba4bc..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/register_feature.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'uptime', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { defaultMessage: 'Uptime' }), - description: i18n.translate('xpack.uptime.featureCatalogueDescription', { - defaultMessage: 'Perform endpoint health checks and uptime monitoring.', - }), - icon: 'uptimeApp', - path: `uptime#/`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts deleted file mode 100644 index 67c75314a7305..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createAction } from 'redux-actions'; -import { createAsyncAction } from './utils'; -import { 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 41381afd31453..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export interface AsyncAction<Payload, SuccessPayload> { - get: (payload: Payload) => Action<Payload>; - success: (payload: SuccessPayload) => Action<SuccessPayload>; - fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; -} -export interface AsyncAction1<Payload, SuccessPayload> { - get: (payload?: Payload) => Action<Payload>; - success: (payload: SuccessPayload) => Action<SuccessPayload>; - fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; -} - -export interface MonitorIdParam { - monitorId: string; -} - -export interface HeartbeatIndicesParam { - heartbeatIndices: string; -} - -export interface QueryParams { - monitorId: string; - dateStart: string; - dateEnd: string; - filters?: string; - statusFilter?: string; - location?: string; -} - -export interface MonitorDetailsActionPayload { - monitorId: string; - dateStart: string; - dateEnd: string; - location?: string; -} - -export interface CreateMLJobSuccess { - count: number; - jobId: string; -} - -export interface DeleteJobResults { - [id: string]: { - [status: string]: boolean; - error?: any; - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts b/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts deleted file mode 100644 index a5adb96731f33..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createAction } from 'redux-actions'; -import { AsyncAction, AsyncAction1 } from './types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export function createAsyncAction<Payload, SuccessPayload>( - actionStr: string -): AsyncAction1<Payload, SuccessPayload>; -export function createAsyncAction<Payload, SuccessPayload>( - actionStr: string -): AsyncAction<Payload, SuccessPayload> { - return { - get: createAction<Payload>(actionStr), - success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`), - fail: createAction<IHttpFetchError>(`${actionStr}_FAIL`), - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts deleted file mode 100644 index 16b90e9921428..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import { apiService } from './utils'; -import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; -import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { 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 e67efa8570c11..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isRight } from 'fp-ts/lib/Either'; -import { HttpFetchQuery, HttpSetup } from '../../../../../../../target/types/core/public'; - -class ApiService { - private static instance: ApiService; - private _http!: HttpSetup; - - public get http() { - return this._http; - } - - public set http(httpSetup: HttpSetup) { - this._http = httpSetup; - } - - private constructor() {} - - static getInstance(): ApiService { - if (!ApiService.instance) { - ApiService.instance = new ApiService(); - } - - return ApiService.instance; - } - - public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) { - const response = await this._http!.get(apiUrl, { query: params }); - - if (decodeType) { - const decoded = decodeType.decode(response); - if (isRight(decoded)) { - return decoded.right; - } else { - // eslint-disable-next-line no-console - console.error( - `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` - ); - } - } - - return response; - } - - public async post(apiUrl: string, data?: any, decodeType?: any) { - const response = await this._http!.post(apiUrl, { - method: 'POST', - body: JSON.stringify(data), - }); - - if (decodeType) { - const decoded = decodeType.decode(response); - if (isRight(decoded)) { - return decoded.right; - } else { - // eslint-disable-next-line no-console - console.warn( - `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` - ); - } - } - return response; - } - - public async delete(apiUrl: string) { - const response = await this._http!.delete(apiUrl); - if (response instanceof Error) { - throw response; - } - return response; - } -} - -export const apiService = ApiService.getInstance(); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts deleted file mode 100644 index df5e825c3488b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { handleActions } from 'redux-actions'; -import { - getExistingMLJobAction, - createMLJobAction, - getAnomalyRecordsAction, - deleteMLJobAction, - resetMLState, - AnomalyRecords, - getMLCapabilitiesAction, -} from '../actions'; -import { getAsyncInitialState, handleAsyncAction } from './utils'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; -import { AsyncInitialState } from './types'; -import { 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 88995a2f5dd70..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export interface AsyncInitialState<ReduceStateType> { - data: ReduceStateType | null; - loading: boolean; - error?: IHttpFetchError | null; -} diff --git a/x-pack/legacy/plugins/uptime/tsconfig.json b/x-pack/legacy/plugins/uptime/tsconfig.json deleted file mode 100644 index 53425909db3e8..0000000000000 --- a/x-pack/legacy/plugins/uptime/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["**/node_modules/**"], - "paths": { - "react": ["../../../node_modules/@types/react"] - } -} \ No newline at end of file diff --git a/x-pack/package.json b/x-pack/package.json index ca73884b8c7c7..dcc9b8c61cb96 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -20,6 +20,11 @@ "build": { "intermediateBuildDirectory": "build/plugin/kibana/x-pack", "oss": false + }, + "clean": { + "extraPatterns": [ + "plugins/*/target" + ] } }, "resolutions": { @@ -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", @@ -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/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 59f8f3747273f..101e18f2583e3 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -140,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/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 10e4d64584340..a6cc1fb5463bb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -53,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 = { @@ -133,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({ diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts new file mode 100644 index 0000000000000..dbd7925f96871 --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'action', + hidden: false, + namespaceType: 'single', + mappings: mappings.action, + }); + + // Encrypted attributes + // - `secrets` properties will be encrypted + // - `config` will be included in AAD + // - everything else excluded from AAD + encryptedSavedObjects.registerType({ + type: 'action', + attributesToEncrypt: new Set(['secrets']), + attributesToExcludeFromAAD: new Set(['name']), + }); + + savedObjects.registerType({ + name: 'action_task_params', + hidden: false, + namespaceType: 'single', + mappings: mappings.action_task_params, + }); + encryptedSavedObjects.registerType({ + type: 'action_task_params', + attributesToEncrypt: new Set(['apiKey']), + }); +} diff --git a/x-pack/legacy/plugins/actions/server/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/actions/server/mappings.json rename to x-pack/plugins/actions/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index c03d3506a051d..8cdde2eeb9877 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -58,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 = { @@ -134,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({ diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts new file mode 100644 index 0000000000000..4efec2fe55ef0 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'alert', + hidden: false, + namespaceType: 'single', + mappings: mappings.alert, + }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set([ + 'scheduledTaskId', + 'muteAll', + 'mutedInstanceIds', + 'updatedBy', + ]), + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/alerting/server/mappings.json rename to x-pack/plugins/alerting/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 8b14199b7276a..26d8a1d1777c0 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 @@ -165,6 +165,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -226,6 +227,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -342,6 +344,7 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ @@ -558,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 9c8cf4b1c968d..26970dc6b2b0d 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/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 f4bda2af653aa..0e4586f707f42 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 a7d6aa894d91d..6c635cc5b4489 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -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 0000000000000..85e1643d684f2 --- /dev/null +++ b/x-pack/plugins/endpoint/common/alert_constants.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class AlertConstants { + /** + * The prefix for all Alert APIs + */ + static BASE_API_URL = '/api/endpoint'; + /** + * The path for the Alert's Index Pattern API. + */ + static INDEX_PATTERN_ROUTE = `${AlertConstants.BASE_API_URL}/index_pattern`; + /** + * Alert's Index pattern + */ + static ALERT_INDEX_NAME = 'events-endpoint-1'; + /** + * A paramter passed to Alert's Index Pattern. + */ + static EVENT_DATASET = 'events'; + /** + * Alert's Search API default page size + */ + static DEFAULT_TOTAL_HITS = 10000; + /** + * Alerts + **/ + static ALERT_LIST_DEFAULT_PAGE_SIZE = 10; + static ALERT_LIST_DEFAULT_SORT = '@timestamp'; + static MAX_LONG_INT = '9223372036854775807'; // 2^63-1 +} diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 4f5a6eaeb0a5e..e40fc3e386bc8 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -9,9 +9,9 @@ import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, - HostFields, + Host, HostMetadata, - OSFields, + HostOS, PolicyData, HostPolicyResponse, HostPolicyResponseActionStatus, @@ -29,7 +29,7 @@ interface EventOptions { processName?: string; } -const Windows: OSFields[] = [ +const Windows: HostOS[] = [ { name: 'windows 10.0', full: 'Windows 10', @@ -56,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 }> = [ { @@ -102,7 +102,7 @@ interface HostInfo { version: string; id: string; }; - host: HostFields; + host: Host; endpoint: { policy: { id: string; @@ -307,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(), }, }; } @@ -645,3 +645,16 @@ export class EndpointDocGenerator { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } } + +const fakeProcessNames = [ + 'lsass.exe', + 'notepad.exe', + 'mimikatz.exe', + 'powershell.exe', + 'iexlorer.exe', + 'explorer.exe', +]; +/** Return a random fake process name */ +function randomProcessName(): string { + return fakeProcessNames[Math.floor(Math.random() * fakeProcessNames.length)]; +} diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts index 650486f3c3858..47f39d2d11797 100644 --- a/x-pack/plugins/endpoint/common/models/event.ts +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent, LegacyEndpointEvent } from '../types'; +import { LegacyEndpointEvent, ResolverEvent } from '../types'; -export function isLegacyEvent( - event: EndpointEvent | LegacyEndpointEvent -): event is LegacyEndpointEvent { +export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent { return (event as LegacyEndpointEvent).endgame !== undefined; } -export function eventTimestamp( - event: EndpointEvent | LegacyEndpointEvent -): string | undefined | number { +export function eventTimestamp(event: ResolverEvent): string | undefined | number { if (isLegacyEvent(event)) { return event.endgame.timestamp_utc; } else { @@ -22,10 +18,31 @@ export function eventTimestamp( } } -export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { +export function eventName(event: ResolverEvent): string { if (isLegacyEvent(event)) { return event.endgame.process_name ? event.endgame.process_name : ''; } else { return event.process.name; } } + +export function eventId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.serial_event_id ? String(event.endgame.serial_event_id) : ''; + } + return event.event.id; +} + +export function entityId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.unique_pid ? String(event.endgame.unique_pid) : ''; + } + return event.process.entity_id; +} + +export function parentEntityId(event: ResolverEvent): string | undefined { + if (isLegacyEvent(event)) { + return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined; + } + return event.process.parent?.entity_id; +} diff --git a/x-pack/plugins/endpoint/common/schema/alert_index.ts b/x-pack/plugins/endpoint/common/schema/alert_index.ts index 7b48780f2d86b..cffc00661515f 100644 --- a/x-pack/plugins/endpoint/common/schema/alert_index.ts +++ b/x-pack/plugins/endpoint/common/schema/alert_index.ts @@ -7,7 +7,7 @@ import { schema, Type } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { decode } from 'rison-node'; -import { EndpointAppConstants } from '../types'; +import { AlertConstants } from '../alert_constants'; /** * Used to validate GET requests against the index of the alerting APIs. @@ -18,7 +18,7 @@ export const alertingIndexGetQuerySchema = schema.object( schema.number({ min: 1, max: 100, - defaultValue: EndpointAppConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, + defaultValue: AlertConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, }) ), page_index: schema.maybe( diff --git a/x-pack/plugins/endpoint/common/schema/resolver.ts b/x-pack/plugins/endpoint/common/schema/resolver.ts new file mode 100644 index 0000000000000..f21307e407fd0 --- /dev/null +++ b/x-pack/plugins/endpoint/common/schema/resolver.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +/** + * Used to validate GET requests for a complete resolver tree. + */ +export const validateTree = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 0, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }), + events: schema.number({ defaultValue: 100, min: 0, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for non process events for a specific event. + */ +export const validateEvents = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + events: schema.number({ defaultValue: 100, min: 1, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for the ancestors of a process event. + */ +export const validateAncestry = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for children of a specified process event. + */ +export const validateChildren = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 10, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 4da4f9bc797ca..8fce15d1c794c 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -25,32 +25,44 @@ export type Immutable<T> = T extends undefined | null | boolean | string | numbe ? ImmutableSet<M> : ImmutableObject<T>; -export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>; -export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>; -export type ImmutableSet<T> = ReadonlySet<Immutable<T>>; -export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }; - -export type Direction = 'asc' | 'desc'; - -export class EndpointAppConstants { - static BASE_API_URL = '/api/endpoint'; - static INDEX_PATTERN_ROUTE = `${EndpointAppConstants.BASE_API_URL}/index_pattern`; - static ALERT_INDEX_NAME = 'events-endpoint-1'; - static EVENT_DATASET = 'events'; - static DEFAULT_TOTAL_HITS = 10000; - /** - * Legacy events are stored in indices with endgame-* prefix - */ - static LEGACY_EVENT_INDEX_NAME = 'endgame-*'; +type ImmutableArray<T> = ReadonlyArray<Immutable<T>>; +type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>; +type ImmutableSet<T> = ReadonlySet<Immutable<T>>; +type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }; - /** - * Alerts - **/ - static ALERT_LIST_DEFAULT_PAGE_SIZE = 10; - static ALERT_LIST_DEFAULT_SORT = '@timestamp'; - static MAX_LONG_INT = '9223372036854775807'; // 2^63-1 +/** + * Values for the Alert APIs 'order' and 'direction' parameters. + */ +export type AlertAPIOrdering = 'asc' | 'desc'; + +export interface ResolverNodeStats { + totalEvents: number; + totalAlerts: number; +} + +export interface ResolverNodePagination { + nextChild?: string | null; + nextEvent?: string | null; + nextAncestor?: string | null; + nextAlert?: string | null; } +/** + * A node that contains pointers to other nodes, arrrays of resolver events, and any metadata associated with resolver specific data + */ +export interface ResolverNode { + id: string; + children: ResolverNode[]; + events: ResolverEvent[]; + lifecycle: ResolverEvent[]; + ancestors?: ResolverNode[]; + pagination: ResolverNodePagination; + stats?: ResolverNodeStats; +} + +/** + * Returned by 'api/endpoint/alerts' + */ export interface AlertResultList { /** * The alerts restricted by page size. @@ -88,6 +100,9 @@ export interface AlertResultList { prev: string | null; } +/** + * Returned by the server via /api/endpoint/metadata + */ export interface HostResultList { /* the hosts restricted by the page size */ hosts: HostInfo[]; @@ -99,43 +114,61 @@ export interface HostResultList { request_page_index: number; } -export interface OSFields { +/** + * Operating System metadata for a host. + */ +export interface HostOS { full: string; name: string; version: string; variant: string; } -export interface HostFields { + +/** + * Host metadata. Describes an endpoint host. + */ +export interface Host { id: string; hostname: string; ip: string[]; mac: string[]; - os: OSFields; + os: HostOS; } -export interface HashFields { + +/** + * A record of hashes for something. Provides hashes in multiple formats. A favorite structure of the Elastic Endpoint. + */ +interface Hashes { + /** + * A hash in MD5 format. + */ md5: string; + /** + * A hash in SHA-1 format. + */ sha1: string; + /** + * A hash in SHA-256 format. + */ sha256: string; } -export interface MalwareClassificationFields { + +interface MalwareClassification { identifier: string; score: number; threshold: number; version: string; } -export interface PrivilegesFields { - description: string; - name: string; - enabled: boolean; -} -export interface ThreadFields { + +interface ThreadFields { id: number; service_name: string; start: number; start_address: number; start_address_module: string; } -export interface DllFields { + +interface DllFields { pe: { architecture: string; imphash: string; @@ -145,8 +178,8 @@ export interface DllFields { trusted: boolean; }; compile_time: number; - hash: HashFields; - malware_classification: MalwareClassificationFields; + hash: Hashes; + malware_classification: MalwareClassification; mapped_address: number; mapped_size: number; path: string; @@ -154,7 +187,6 @@ export interface DllFields { /** * Describes an Alert Event. - * Should be in line with ECS schema. */ export type AlertEvent = Immutable<{ '@timestamp': number; @@ -191,14 +223,14 @@ export type AlertEvent = Immutable<{ entity_id: string; }; name: string; - hash: HashFields; + hash: Hashes; pe?: { imphash: string; }; executable: string; sid?: string; start: number; - malware_classification?: MalwareClassificationFields; + malware_classification?: MalwareClassification; token: { domain: string; type: string; @@ -206,7 +238,11 @@ export type AlertEvent = Immutable<{ sid: string; integrity_level: number; integrity_level_name: string; - privileges?: PrivilegesFields[]; + privileges?: Array<{ + description: string; + name: string; + enabled: boolean; + }>; }; thread?: ThreadFields[]; uptime: number; @@ -220,7 +256,7 @@ export type AlertEvent = Immutable<{ mtime: number; created: number; size: number; - hash: HashFields; + hash: Hashes; pe?: { imphash: string; }; @@ -228,10 +264,10 @@ export type AlertEvent = Immutable<{ trusted: boolean; subject_name: string; }; - malware_classification: MalwareClassificationFields; + malware_classification: MalwareClassification; temp_file_path: string; }; - host: HostFields; + host: Host; dll?: DllFields[]; }>; @@ -249,9 +285,6 @@ interface AlertState { }; } -/** - * Union of alert data and metadata. - */ export type AlertData = AlertEvent & AlertMetadata; export type AlertDetails = AlertData & AlertState; @@ -301,7 +334,7 @@ export type HostMetadata = Immutable<{ id: string; version: string; }; - host: HostFields; + host: Host; }>; /** @@ -365,7 +398,7 @@ export interface EndpointEvent { hostname: string; ip: string[]; mac: string[]; - os: OSFields; + os: HostOS; }; process: { entity_id: string; @@ -500,28 +533,22 @@ export interface PolicyConfig { }; } -/** - * Windows-specific policy configuration that is supported via the UI - */ -type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>; - -/** - * Mac-specific policy configuration that is supported via the UI - */ -type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>; - -/** - * Linux-specific policy configuration that is supported via the UI - */ -type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>; - /** * The set of Policy configuration settings that are show/edited via the UI */ export interface UIPolicyConfig { - windows: WindowsPolicyConfig; - mac: MacPolicyConfig; - linux: LinuxPolicyConfig; + /** + * Windows-specific policy configuration that is supported via the UI + */ + windows: Pick<PolicyConfig['windows'], 'events' | 'malware'>; + /** + * Mac-specific policy configuration that is supported via the UI + */ + mac: Pick<PolicyConfig['mac'], 'malware' | 'events'>; + /** + * Linux-specific policy configuration that is supported via the UI + */ + linux: Pick<PolicyConfig['linux'], 'events'>; } interface PolicyConfigAdvancedOptions { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 90d6b8b82198a..6bc728db99819 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -9,7 +9,7 @@ import { AlertResultList, AlertDetails } from '../../../../../common/types'; import { ImmutableMiddlewareFactory, AlertListState } from '../../types'; import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query'; -import { EndpointAppConstants } from '../../../../../common/types'; +import { AlertConstants } from '../../../../../common/alert_constants'; export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> = ( coreStart, @@ -18,7 +18,7 @@ export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> async function fetchIndexPatterns(): Promise<IIndexPattern[]> { const { indexPatterns } = depsStart.data; const eventsPattern: { indexPattern: string } = await coreStart.http.get( - `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/${EndpointAppConstants.EVENT_DATASET}` + `${AlertConstants.INDEX_PATTERN_ROUTE}/${AlertConstants.EVENT_DATASET}` ); const fields = await indexPatterns.getFieldsForWildcard({ pattern: eventsPattern.indexPattern, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 515c54eab3280..863ffc50d0155 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -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 1af83a975d1d8..2064c76f7dfb5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -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/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index eb74c40ff3687..93e995194353b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -38,7 +38,7 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = ( } = action.payload; return { ...state, - hosts: hosts.map(hostInfo => hostInfo.metadata), + hosts, total, pageSize, pageIndex, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts index 69b11fb3c1f0e..9912c9a81e6e1 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts @@ -16,6 +16,7 @@ import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; import { Immutable } from '../../../../../common/types'; import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; describe('policy list store concerns', () => { let fakeCoreStart: ReturnType<typeof coreMock.createStart>; @@ -121,7 +122,11 @@ describe('policy list store concerns', () => { }); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); @@ -140,7 +145,11 @@ describe('policy list store concerns', () => { dispatchUserChangedUrl('?page_size=50&page_index=0'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 50 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 50, + }, }); }); it('uses defaults for params not in url', async () => { @@ -159,21 +168,33 @@ describe('policy list store concerns', () => { dispatchUserChangedUrl('?page_size=-50&page_index=-99'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); it('it ignores non-numeric values for page_index and page_size', async () => { dispatchUserChangedUrl('?page_size=fifty&page_index=ten'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); it('accepts only known values for `page_size`', async () => { dispatchUserChangedUrl('?page_size=300&page_index=10'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 11, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 11, + perPage: 10, + }, }); }); it(`ignores unknown url search params`, async () => { @@ -186,8 +207,8 @@ describe('policy list store concerns', () => { it(`uses last param value if param is defined multiple times`, async () => { dispatchUserChangedUrl('?page_size=20&page_size=50&page_index=20&page_index=40'); expect(urlSearchParams(getState())).toEqual({ - page_index: 20, - page_size: 20, + page_index: 40, + page_size: 50, }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts index 6d2e952fa07bb..4986a342cca19 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts @@ -46,12 +46,16 @@ export const urlSearchParams: ( const query = parse(location.search); // Search params can appear multiple times in the URL, in which case the value for them, - // once parsed, would be an array. In these case, we take the first value defined + // once parsed, would be an array. In these case, we take the last value defined searchParams.page_index = Number( - (Array.isArray(query.page_index) ? query.page_index[0] : query.page_index) ?? 0 + (Array.isArray(query.page_index) + ? query.page_index[query.page_index.length - 1] + : query.page_index) ?? 0 ); searchParams.page_size = Number( - (Array.isArray(query.page_size) ? query.page_size[0] : query.page_size) ?? 10 + (Array.isArray(query.page_size) + ? query.page_size[query.page_size.length - 1] + : query.page_size) ?? 10 ); // If pageIndex is not a valid positive integer, set it to 0 diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts index c2865d36c95f2..46f4c09e05a74 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts @@ -6,6 +6,7 @@ import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; import { httpServiceMock } from '../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; describe('ingest service', () => { let http: ReturnType<typeof httpServiceMock.createStartContract>; @@ -19,7 +20,7 @@ describe('ingest service', () => { await sendGetEndpointSpecificDatasources(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { query: { - kuery: 'datasources.package.name: endpoint', + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }); @@ -29,7 +30,7 @@ describe('ingest service', () => { }); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { query: { - kuery: 'someValueHere and datasources.package.name: endpoint', + kuery: `someValueHere and ${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, perPage: 10, page: 1, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts index 4356517e43c2c..5c27680d6a35c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts @@ -8,6 +8,7 @@ import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { GetDatasourcesRequest, GetAgentStatusResponse, + DATASOURCE_SAVED_OBJECT_TYPE, } from '../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; import { NewPolicyData } from '../../../../../../common/types'; @@ -33,7 +34,7 @@ export const sendGetEndpointSpecificDatasources = ( ...options.query, kuery: `${ options?.query?.kuery ? options.query.kuery + ' and ' : '' - }datasources.package.name: endpoint`, + }${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 0598ce5f38efa..58e706f20ec8e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -17,12 +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'; @@ -91,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 */ @@ -311,7 +311,7 @@ export type AlertListData = AlertResultList; export interface AlertListState { /** Array of alert items. */ - readonly alerts: ImmutableArray<AlertData>; + readonly alerts: Immutable<AlertData[]>; /** The total number of alerts on the page. */ readonly total: number; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index d18bc59a35f52..d32ad4dd9defc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -33,5 +33,6 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; - min-height: 500px; + /* gross demo hack */ + min-height: calc(100vh - 505px); `; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index 499efb4f4b8ed..5a8765110c909 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -14,7 +14,7 @@ import { mockHostResultList, } from '../../store/hosts/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; -import { HostInfo, HostPolicyResponseActionStatus } from '../../../../../common/types'; +import { HostInfo, HostStatus, HostPolicyResponseActionStatus } from '../../../../../common/types'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; describe('when on the hosts page', () => { @@ -48,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(); @@ -76,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]); } }); 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 5c2922162ce0c..026ba2ff15126 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -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 ea9eb292dba1a..d9bb7eabcf7b0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -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 7f946de9614ca..9d73f12869058 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx @@ -17,18 +17,18 @@ import { } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../../models/policy_details_config'; -import { UIPolicyConfig, ImmutableArray } from '../../../../../../../common/types'; +import { UIPolicyConfig, Immutable } from '../../../../../../../common/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); const total = usePolicyDetailsSelector(totalWindowsEvents); const checkboxes = useMemo(() => { - const items: ImmutableArray<{ + const items: Immutable<Array<{ name: string; os: 'windows'; protectionField: keyof UIPolicyConfig['windows']['events']; - }> = [ + }>> = [ { name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { defaultMessage: 'DLL and Driver Load', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx index 14871c71ec038..4b2a6ece9f58f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator } from '@elastic/eui'; -import { Immutable, ProtectionModes, ImmutableArray } from '../../../../../../../common/types'; +import { Immutable, ProtectionModes } from '../../../../../../../common/types'; import { OS, MalwareProtectionOSes } from '../../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../../store/policy_details/selectors'; @@ -73,11 +73,11 @@ export const MalwareProtections = React.memo(() => { // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const radios: ImmutableArray<{ + const radios: Immutable<Array<{ id: ProtectionModes; label: string; protection: 'malware'; - }> = useMemo(() => { + }>> = useMemo(() => { return [ { id: ProtectionModes.detect, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 373afa89921dc..3ec15f2f1985d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -8,13 +8,11 @@ import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - readonly payload: { - readonly data: { - readonly result: { - readonly search_results: readonly ResolverEvent[]; - }; - }; - }; + readonly payload: ResolverEvent[]; } -export type DataAction = ServerReturnedResolverData; +interface ServerFailedToReturnResolverData { + readonly type: 'serverFailedToReturnResolverData'; +} + +export type DataAction = ServerReturnedResolverData | ServerFailedToReturnResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index f01136fe20ebf..f95ecc63d2a66 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -8,7 +8,7 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; @@ -113,13 +113,7 @@ describe('resolver graph layout', () => { }); describe('when rendering no nodes', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [], - }, - }, - }; + const payload: ResolverEvent[] = []; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -133,13 +127,7 @@ describe('resolver graph layout', () => { }); describe('when rendering one node', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA], - }, - }, - }; + const payload = [processA]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -153,13 +141,7 @@ describe('resolver graph layout', () => { }); describe('when rendering two nodes, one being the parent of the other', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA, processB], - }, - }, - }; + const payload = [processA, processB]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -173,23 +155,17 @@ describe('resolver graph layout', () => { }); describe('when rendering two forks, and one fork has an extra long tine', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [ - processA, - processB, - processC, - processD, - processE, - processF, - processG, - processH, - processI, - ], - }, - }, - }; + const payload = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + processH, + processI, + ]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index a3184389a794e..fc307002819a9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -11,25 +11,28 @@ function initialState(): DataState { return { results: [], isLoading: false, + hasError: false, }; } export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState(), action) => { if (action.type === 'serverReturnedResolverData') { - const { - data: { - result: { search_results }, - }, - } = action.payload; return { ...state, - results: search_results, + results: action.payload, isLoading: false, + hasError: false, }; } else if (action.type === 'appRequestedResolverData') { return { ...state, isLoading: true, + hasError: false, + }; + } else if (action.type === 'serverFailedToReturnResolverData') { + return { + ...state, + hasError: true, }; } else { return state; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 5dda54d4ed029..59ee4b3b87505 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -34,6 +34,10 @@ export function isLoading(state: DataState) { return state.isLoading; } +export function hasError(state: DataState) { + return state.hasError; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -293,7 +297,7 @@ function* levelOrderWithWidths( metadata.firstChildWidth = width; } else { const firstChildWidth = widths.get(siblings[0]); - const lastChildWidth = widths.get(siblings[0]); + const lastChildWidth = widths.get(siblings[siblings.length - 1]); if (firstChildWidth === undefined || lastChildWidth === undefined) { /** * All widths have been precalcluated, so this will not happen. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 4e57212e5c0c2..c7177c6387e7a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,7 +8,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; -import { ResolverEvent } from '../../../../common/types'; +import { ResolverEvent, ResolverNode } from '../../../../common/types'; import * as event from '../../../../common/models/event'; type MiddlewareFactory<S = ResolverState> = ( @@ -16,18 +16,18 @@ type MiddlewareFactory<S = ResolverState> = ( ) => ( api: MiddlewareAPI<Dispatch<ResolverAction>, S> ) => (next: Dispatch<ResolverAction>) => (action: ResolverAction) => unknown; -interface Lifecycle { - lifecycle: ResolverEvent[]; -} -type ChildResponse = [Lifecycle]; -function flattenEvents(events: ChildResponse): ResolverEvent[] { - return events - .map((child: Lifecycle) => child.lifecycle) - .reduce( - (accumulator: ResolverEvent[], value: ResolverEvent[]) => accumulator.concat(value), - [] - ); +function flattenEvents(children: ResolverNode[], events: ResolverEvent[] = []): ResolverEvent[] { + return children.reduce((flattenedEvents, currentNode) => { + if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { + flattenedEvents.push(...currentNode.lifecycle); + } + if (currentNode.children && currentNode.children.length > 0) { + return flattenEvents(currentNode.children, events); + } else { + return flattenedEvents; + } + }, events); } export const resolverMiddlewareFactory: MiddlewareFactory = context => { @@ -39,53 +39,43 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { */ if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - let response = []; - let lifecycle: ResolverEvent[]; - let childEvents: ResolverEvent[]; - let relatedEvents: ResolverEvent[]; - let children = []; - const ancestors: ResolverEvent[] = []; - const maxAncestors = 5; - if (event.isLegacyEvent(action.payload.selectedEvent)) { - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - childEvents = children.length > 0 ? flattenEvents(children) : []; - } else { - const uniquePid = action.payload.selectedEvent.process.entity_id; - const ppid = action.payload.selectedEvent.process.parent?.entity_id; - async function getAncestors(pid: string | undefined) { - if (ancestors.length < maxAncestors && pid !== undefined) { - const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); - ancestors.push(parent.lifecycle[0]); - if (parent.lifecycle[0].process?.parent?.entity_id) { - await getAncestors(parent.lifecycle[0].process.parent.entity_id); - } - } + try { + let lifecycle: ResolverEvent[]; + let children: ResolverNode[]; + let ancestors: ResolverNode[]; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const entityId = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { legacyEndpointID, children: 5, ancestors: 5 }, + }), + ]); + } else { + const entityId = action.payload.selectedEvent.process.entity_id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { + children: 5, + ancestors: 5, + }, + }), + ]); } - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), - getAncestors(ppid), - ]); + const response: ResolverEvent[] = [ + ...lifecycle, + ...flattenEvents(children), + ...flattenEvents(ancestors), + ]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: response, + }); + } catch (error) { + api.dispatch({ + type: 'serverFailedToReturnResolverData', + }); } - childEvents = children.length > 0 ? flattenEvents(children) : []; - response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; - api.dispatch({ - type: 'serverReturnedResolverData', - payload: { data: { result: { search_results: response } } }, - }); } } }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index e8ae3d08e5cb6..7d09d90881da9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -102,6 +102,11 @@ function uiStateSelector(state: ResolverState) { */ export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); +/** + * Whether or not the resolver encountered an error while fetching data + */ +export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index d370bda0d1842..17aa598720c59 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -136,6 +136,7 @@ export type CameraState = { export interface DataState { readonly results: readonly ResolverEvent[]; isLoading: boolean; + hasError: boolean; } export type Vector2 = readonly [number, number]; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 36155ece57a9c..2e7ca65c92dc1 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -8,6 +8,7 @@ import React, { useLayoutEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; @@ -59,6 +60,7 @@ export const Resolver = styled( const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); + const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.uiActiveDescendantId); useLayoutEffect(() => { @@ -74,6 +76,16 @@ export const Resolver = styled( <div className="loading-container"> <EuiLoadingSpinner size="xl" /> </div> + ) : hasError ? ( + <div className="loading-container"> + <div> + {' '} + <FormattedMessage + id="xpack.endpoint.resolver.loadingError" + defaultMessage="Error loading data." + /> + </div> + </div> ) : ( <StyledResolverContainer className="resolver-graph kbn-resetFocusState" diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 6e83fc19a922e..1545e9c2ae6e8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -153,13 +153,7 @@ describe('useCamera on an unpainted element', () => { } const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { - data: { - result: { - search_results: events, - }, - }, - }, + payload: events, }; act(() => { store.dispatch(serverResponseAction); diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index dcedd27fc5a3d..903d48746bfb3 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 e438ab853f3b5..92f8aacbf26a2 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -5,7 +5,8 @@ */ import { GetResponse } from 'elasticsearch'; import { KibanaRequest, RequestHandler } from 'kibana/server'; -import { AlertEvent, EndpointAppConstants } from '../../../../common/types'; +import { AlertEvent } from '../../../../common/types'; +import { AlertConstants } from '../../../../common/alert_constants'; import { EndpointAppContext } from '../../../types'; import { AlertDetailsRequestParams } from '../types'; import { AlertDetailsPagination } from './lib'; @@ -22,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 d482da03872c6..0f69e1bb60c44 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts @@ -5,12 +5,8 @@ */ import { GetResponse, SearchResponse } from 'elasticsearch'; -import { - AlertEvent, - AlertHits, - Direction, - EndpointAppConstants, -} from '../../../../../common/types'; +import { AlertEvent, AlertHits, AlertAPIOrdering } from '../../../../../common/types'; +import { AlertConstants } from '../../../../../common/alert_constants'; import { EndpointConfigType } from '../../../../config'; import { searchESForAlerts, Pagination } from '../../lib'; import { AlertSearchQuery, SearchCursor, AlertDetailsRequestParams } from '../../types'; @@ -36,12 +32,12 @@ export class AlertDetailsPagination extends Pagination< } protected async doSearch( - direction: Direction, + direction: AlertAPIOrdering, cursor: SearchCursor ): Promise<SearchResponse<AlertEvent>> { const reqData: AlertSearchQuery = { pageSize: 1, - sort: EndpointAppConstants.ALERT_LIST_DEFAULT_SORT, + sort: AlertConstants.ALERT_LIST_DEFAULT_SORT, order: 'desc', query: { query: '', language: 'kuery' }, filters: [] as Filter[], diff --git a/x-pack/plugins/endpoint/server/routes/alerts/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/index.ts index 09de11897813b..b61f90b5b17f5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/index.ts @@ -5,12 +5,12 @@ */ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../../types'; -import { EndpointAppConstants } from '../../../common/types'; +import { AlertConstants } from '../../../common/alert_constants'; import { alertListHandlerWrapper } from './list'; import { alertDetailsHandlerWrapper, alertDetailsReqSchema } from './details'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; -export const BASE_ALERTS_ROUTE = `${EndpointAppConstants.BASE_API_URL}/alerts`; +export const BASE_ALERTS_ROUTE = `${AlertConstants.BASE_API_URL}/alerts`; export function registerAlertRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.get( diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts index 74db24c85eab5..7bc1c0c306ae2 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts @@ -7,7 +7,8 @@ import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; import { esQuery } from '../../../../../../../src/plugins/data/server'; -import { AlertEvent, Direction, EndpointAppConstants } from '../../../../common/types'; +import { AlertEvent, AlertAPIOrdering } from '../../../../common/types'; +import { AlertConstants } from '../../../../common/alert_constants'; import { AlertSearchQuery, AlertSearchRequest, @@ -18,7 +19,7 @@ import { export { Pagination } from './pagination'; -function reverseSortDirection(order: Direction): Direction { +function reverseSortDirection(order: AlertAPIOrdering): AlertAPIOrdering { if (order === 'asc') { return 'desc'; } @@ -100,13 +101,13 @@ const buildAlertSearchQuery = async ( query: AlertSearchQuery, indexPattern: string ): Promise<AlertSearchRequestWrapper> => { - let totalHitsMin: number = EndpointAppConstants.DEFAULT_TOTAL_HITS; + let totalHitsMin: number = AlertConstants.DEFAULT_TOTAL_HITS; // Calculate minimum total hits set to indicate there's a next page if (query.fromIndex) { totalHitsMin = Math.max( query.fromIndex + query.pageSize * 2, - EndpointAppConstants.DEFAULT_TOTAL_HITS + AlertConstants.DEFAULT_TOTAL_HITS ); } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 95c8e4662cfce..92bd07a813d26 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 cf4eac5d25f80..5aefc35b5758f 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/types.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/types.ts @@ -5,14 +5,14 @@ */ import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; -import { Direction } from '../../../common/types'; +import { AlertAPIOrdering } from '../../../common/types'; /** * Sort parameters for alerts in ES. */ export interface AlertSortParam { [key: string]: { - order: Direction; + order: AlertAPIOrdering; missing?: UndefinedResultPosition; }; } @@ -38,7 +38,7 @@ export interface AlertSearchQuery { filters: Filter[]; dateRange?: TimeRange; sort: string; - order: Direction; + order: AlertAPIOrdering; searchAfter?: SearchCursor; searchBefore?: SearchCursor; emptyStringIsUndefined?: boolean; @@ -83,7 +83,7 @@ export interface AlertListRequestQuery { filters?: string; date_range: string; sort: string; - order: Direction; + order: AlertAPIOrdering; after?: SearchCursor; before?: SearchCursor; empty_string_is_undefined?: boolean; diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts index 79083f5f05e14..7e78caaf178e4 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 a96d431225b15..3599acacb4f59 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 0000000000000..6648dc5b9e493 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateAncestry } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleAncestry( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateAncestry.params>, TypeOf<typeof validateAncestry.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { ancestors, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.ancestors(ancestors + 1); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index c3b19b6c912b6..bb18b29a4b947 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 0000000000000..a70a6e8d097d0 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/events.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler, Logger } from 'kibana/server'; +import { validateEvents } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleEvents( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateEvents.params>, TypeOf<typeof validateEvents.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { events, afterEvent, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.events(events, afterEvent); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts deleted file mode 100644 index 91a4c5d49bc54..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { 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 b049439207e50..eba4e5581c136 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts @@ -4,10 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; -import { EndpointAppConstants } from '../../../../common/types'; -import { paginate, paginatedResults, PaginationParams } from '../utils/pagination'; +import { ResolverEvent } from '../../../../common/types'; +import { + paginate, + paginatedResults, + PaginationParams, + PaginatedResults, +} from '../utils/pagination'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; export abstract class ResolverQuery { constructor( @@ -16,22 +23,28 @@ export abstract class ResolverQuery { private readonly pagination?: PaginationParams ) {} - protected paginateBy(field: string, query: JsonObject) { - if (!this.pagination) { - return query; - } - return paginate(this.pagination, field, query); + protected paginateBy(tiebreaker: string, aggregator: string) { + return (query: JsonObject) => { + if (!this.pagination) { + return query; + } + return paginate(this.pagination, tiebreaker, aggregator, query); + }; } build(...ids: string[]) { if (this.endpointID) { - return this.legacyQuery(this.endpointID, ids, EndpointAppConstants.LEGACY_EVENT_INDEX_NAME); + return this.legacyQuery(this.endpointID, ids, legacyEventIndexPattern); } return this.query(ids, this.indexPattern); } async search(client: IScopedClusterClient, ...ids: string[]) { - return paginatedResults(await client.callAsCurrentUser('search', this.build(...ids))); + return this.postSearch(await client.callAsCurrentUser('search', this.build(...ids))); + } + + protected postSearch(response: SearchResponse<ResolverEvent>): PaginatedResults { + return paginatedResults(response); } protected abstract legacyQuery( diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index e73053d53dee0..2a097e87c38b2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { ChildrenQuery } from './children'; -import { EndpointAppConstants } from '../../../../common/types'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; export const fakeEventIndexPattern = 'events-endpoint-*'; @@ -12,7 +12,7 @@ describe('children events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + new ChildrenQuery(legacyEventIndexPattern, 'awesome-id', { size: 1, timestamp, eventID: 'foo', @@ -32,15 +32,28 @@ describe('children events query', () => { term: { 'event.category': 'process' }, }, { - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, }, aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', + totals: { + terms: { + field: 'endgame.unique_ppid', + size: 1, }, }, }, @@ -48,7 +61,7 @@ describe('children events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, + index: legacyEventIndexPattern, }); }); @@ -67,20 +80,14 @@ describe('children events query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': ['baz'] }, - }, - { - terms: { 'process.parent.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.parent.entity_id': ['baz'] }, }, { term: { 'event.category': 'process' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.type': 'start' }, }, @@ -88,9 +95,10 @@ describe('children events query', () => { }, }, aggs: { - total: { - value_count: { - field: 'event.id', + totals: { + terms: { + field: 'process.parent.entity_id', + size: 1, }, }, }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts index 6d084a0cf20e5..690c926d7e6d6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts @@ -7,8 +7,9 @@ import { ResolverQuery } from './base'; export class ChildrenQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string) { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_ppid'); return { - body: this.paginateBy('endgame.serial_event_id', { + body: paginator({ query: { bool: { filter: [ @@ -22,11 +23,19 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, @@ -37,31 +46,22 @@ export class ChildrenQuery extends ResolverQuery { } protected query(entityIDs: string[], index: string) { + const paginator = this.paginateBy('event.id', 'process.parent.entity_id'); return { - body: this.paginateBy('event.id', { + body: paginator({ query: { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': entityIDs }, - }, - { - terms: { 'process.parent.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.parent.entity_id': entityIDs }, }, { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 + term: { 'event.kind': 'event' }, + }, + { term: { 'event.type': 'start' }, }, ], diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts new file mode 100644 index 0000000000000..78e5ee9226581 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EventsQuery } from './events'; +import { fakeEventIndexPattern } from './children.test'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; + +describe('related events query', () => { + it('generates the correct legacy queries', () => { + const timestamp = new Date().getTime(); + expect( + new EventsQuery(legacyEventIndexPattern, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') + ).toStrictEqual({ + body: { + query: { + bool: { + filter: [ + { + terms: { 'endgame.unique_pid': ['5'] }, + }, + { + term: { 'agent.id': 'awesome-id' }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + aggs: { + totals: { + terms: { + field: 'endgame.unique_pid', + size: 1, + }, + }, + }, + search_after: [timestamp, 'foo'], + size: 1, + sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], + }, + index: legacyEventIndexPattern, + }); + }); + + it('generates the correct non-legacy queries', () => { + const timestamp = new Date().getTime(); + + expect( + new EventsQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') + ).toStrictEqual({ + body: { + query: { + bool: { + filter: [ + { + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + aggs: { + totals: { + terms: { + field: 'process.entity_id', + size: 1, + }, + }, + }, + search_after: [timestamp, 'bar'], + size: 1, + sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], + }, + index: fakeEventIndexPattern, + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts new file mode 100644 index 0000000000000..b622cb8a21111 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ResolverQuery } from './base'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; + +export class EventsQuery extends ResolverQuery { + protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_pid'); + return { + body: paginator({ + query: { + bool: { + filter: [ + { + terms: { 'endgame.unique_pid': uniquePIDs }, + }, + { + term: { 'agent.id': endpointID }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + }), + index, + }; + } + + protected query(entityIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('event.id', 'process.entity_id'); + return { + body: paginator({ + query: { + bool: { + filter: [ + { + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + }), + index, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts new file mode 100644 index 0000000000000..01e818c89b3ef --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Legacy events are stored in indices with endgame-* prefix + */ +export const legacyEventIndexPattern = 'endgame-*'; diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts index 8a3955706b278..296135af83b72 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts @@ -3,15 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EndpointAppConstants } from '../../../../common/types'; + import { LifecycleQuery } from './lifecycle'; import { fakeEventIndexPattern } from './children.test'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; describe('lifecycle query', () => { it('generates the correct legacy queries', () => { - expect( - new LifecycleQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id').build('5') - ).toStrictEqual({ + expect(new LifecycleQuery(legacyEventIndexPattern, 'awesome-id').build('5')).toStrictEqual({ body: { query: { bool: { @@ -22,15 +21,19 @@ describe('lifecycle query', () => { { term: { 'agent.id': 'awesome-id' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, + index: legacyEventIndexPattern, }); }); @@ -41,16 +44,10 @@ describe('lifecycle query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -58,6 +55,7 @@ describe('lifecycle query', () => { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index: fakeEventIndexPattern, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts index 290c601e0e9d8..e775b0cf9b6d2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts @@ -6,7 +6,6 @@ import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; -// consider limiting the response size to a reasonable value in case we have a bunch of lifecycle events export class LifecycleQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { return { @@ -20,12 +19,16 @@ export class LifecycleQuery extends ResolverQuery { { term: { 'agent.id': endpointID }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, @@ -39,16 +42,10 @@ export class LifecycleQuery extends ResolverQuery { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -56,6 +53,7 @@ export class LifecycleQuery extends ResolverQuery { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts deleted file mode 100644 index 5caef935ce621..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { RelatedEventsQuery } from './related_events'; -import { EndpointAppConstants } from '../../../../common/types'; -import { fakeEventIndexPattern } from './children.test'; - -describe('related events query', () => { - it('generates the correct legacy queries', () => { - const timestamp = new Date().getTime(); - expect( - new RelatedEventsQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { - size: 1, - timestamp, - eventID: 'foo', - }).build('5') - ).toStrictEqual({ - body: { - query: { - bool: { - filter: [ - { - terms: { 'endgame.unique_pid': ['5'] }, - }, - { - term: { 'agent.id': 'awesome-id' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', - }, - }, - }, - search_after: [timestamp, 'foo'], - size: 1, - sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], - }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, - }); - }); - - it('generates the correct non-legacy queries', () => { - const timestamp = new Date().getTime(); - - expect( - new RelatedEventsQuery(fakeEventIndexPattern, undefined, { - size: 1, - timestamp, - eventID: 'bar', - }).build('baz') - ).toStrictEqual({ - body: { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - aggs: { - total: { - value_count: { - field: 'event.id', - }, - }, - }, - search_after: [timestamp, 'bar'], - size: 1, - sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], - }, - index: fakeEventIndexPattern, - }); - }); -}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts deleted file mode 100644 index cc5afe8face8d..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { ResolverQuery } from './base'; -import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; - -export class RelatedEventsQuery extends ResolverQuery { - protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { - return { - body: this.paginateBy('endgame.serial_event_id', { - query: { - bool: { - filter: [ - { - terms: { 'endgame.unique_pid': uniquePIDs }, - }, - { - term: { 'agent.id': endpointID }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - }), - index, - }; - } - - protected query(entityIDs: string[], index: string): JsonObject { - return { - body: this.paginateBy('event.id', { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - }), - index, - }; - } -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts new file mode 100644 index 0000000000000..17a158aec7cf5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; +import { StatsQuery } from './stats'; +import { fakeEventIndexPattern } from './children.test'; + +describe('stats query', () => { + it('generates the correct legacy queries', () => { + expect(new StatsQuery(legacyEventIndexPattern, 'awesome-id').build('5')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'agent.id': 'awesome-id', + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + terms: { + 'endgame.unique_pid': ['5'], + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.kind': 'alert', + }, + }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': ['5'], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.data.alert_details.acting_process.unique_pid', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.unique_pid', + }, + }, + }, + }, + }, + }, + index: legacyEventIndexPattern, + }); + }); + + it('generates the correct non-legacy queries', () => { + expect(new StatsQuery(fakeEventIndexPattern).build('baz')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + 'process.entity_id': ['baz'], + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + term: { + 'event.kind': 'alert', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + }, + }, + index: fakeEventIndexPattern, + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts new file mode 100644 index 0000000000000..7db3ab2b0cb1f --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SearchResponse } from 'elasticsearch'; +import { ResolverQuery } from './base'; +import { ResolverEvent } from '../../../../common/types'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; +import { PaginatedResults } from '../utils/pagination'; + +export class StatsQuery extends ResolverQuery { + protected postSearch(response: SearchResponse<ResolverEvent>): PaginatedResults { + const alerts = response.aggregations.alerts.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + const events = response.aggregations.events.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + return { + totals: {}, + results: [], + extras: { + alerts, + events, + }, + }; + } + + protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { 'agent.id': endpointID }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { terms: { 'endgame.unique_pid': uniquePIDs } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { term: { 'event.kind': 'alert' } }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': uniquePIDs, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'endgame.data.alert_details.acting_process.unique_pid' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'endgame.unique_pid' } }, + }, + }, + }, + }, + index, + }; + } + + protected query(entityIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { 'process.entity_id': entityIDs } }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { term: { 'event.kind': 'alert' } }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + }, + }, + index, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts deleted file mode 100644 index 83e111a1e62e6..0000000000000 --- 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 0000000000000..25f15586341d5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/tree.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateTree } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { Tree } from './utils/tree'; +import { EndpointAppContext } from '../../types'; + +export function handleTree( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateTree.params>, TypeOf<typeof validateTree.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { + children, + generations, + ancestors, + events, + afterEvent, + afterChild, + legacyEndpointID: endpointID, + }, + } = req; + try { + const client = context.core.elasticsearch.dataClient; + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await Tree.merge( + fetcher.children(children, generations, afterChild), + fetcher.ancestors(ancestors + 1), + fetcher.events(events, afterEvent) + ); + + const enrichedTree = await fetcher.stats(tree); + + return res.ok({ + body: enrichedTree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: 'Error retrieving tree.' }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts new file mode 100644 index 0000000000000..7315b4ee6c618 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { getPaginationParams } from './pagination'; +import { Tree } from './tree'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { ChildrenQuery } from '../queries/children'; +import { EventsQuery } from '../queries/events'; +import { StatsQuery } from '../queries/stats'; + +export class Fetcher { + constructor( + private readonly client: IScopedClusterClient, + private readonly id: string, + private readonly indexPattern: string, + private readonly endpointID?: string + ) {} + + public async ancestors(limit: number): Promise<Tree> { + const tree = new Tree(this.id); + await this.doAncestors(tree, this.id, this.id, limit); + return tree; + } + + public async children(limit: number, generations: number, after?: string): Promise<Tree> { + const tree = new Tree(this.id); + await this.doChildren(tree, [this.id], limit, generations, after); + return tree; + } + + public async events(limit: number, after?: string): Promise<Tree> { + const tree = new Tree(this.id); + await this.doEvents(tree, limit, after); + return tree; + } + + public async stats(tree: Tree): Promise<Tree> { + await this.doStats(tree); + return tree; + } + + private async doAncestors(tree: Tree, curNode: string, previousNode: string, levels: number) { + if (levels === 0) { + tree.setNextAncestor(curNode); + return; + } + + const query = new LifecycleQuery(this.indexPattern, this.endpointID); + const { results } = await query.search(this.client, curNode); + + if (results.length === 0) { + tree.setNextAncestor(null); + return; + } + tree.addAncestor(previousNode, ...results); + + const next = parentEntityId(results[0]); + if (next !== undefined) { + await this.doAncestors(tree, next, curNode, levels - 1); + } + } + + private async doEvents(tree: Tree, limit: number, after?: string) { + const query = new EventsQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + + const { totals, results } = await query.search(this.client, this.id); + tree.addEvent(...results); + tree.paginateEvents(totals, results); + if (results.length === 0) tree.setNextEvent(null); + } + + private async doChildren( + tree: Tree, + ids: string[], + limit: number, + levels: number, + after?: string + ) { + if (levels === 0 || ids.length === 0) return; + + const childrenQuery = new ChildrenQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + const lifecycleQuery = new LifecycleQuery(this.indexPattern, this.endpointID); + + const { totals, results } = await childrenQuery.search(this.client, ...ids); + if (results.length === 0) { + tree.markLeafNode(...ids); + return; + } + + const childIDs = results.map(entityId); + const children = (await lifecycleQuery.search(this.client, ...childIDs)).results; + + tree.addChild(...children); + tree.paginateChildren(totals, results); + tree.markLeafNode(...childIDs); + + await this.doChildren(tree, childIDs, limit * limit, levels - 1); + } + + private async doStats(tree: Tree) { + const statsQuery = new StatsQuery(this.indexPattern, this.endpointID); + const ids = tree.ids(); + const { extras } = await statsQuery.search(this.client, ...ids); + const alerts = extras?.alerts || {}; + const events = extras?.events || {}; + ids.forEach(id => { + tree.addStats(id, { totalAlerts: alerts[id] || 0, totalEvents: events[id] || 0 }); + }); + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts deleted file mode 100644 index 6d5ac8efdc1da..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ResolverEvent } from '../../../../common/types'; -import { isLegacyEvent } from '../../../../common/models/event'; - -export function extractEventID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.serial_event_id); - } - return event.event.id; -} - -export function extractEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.unique_pid); - } - return event.process.entity_id; -} - -export function extractParentEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - const ppid = event.endgame.unique_ppid; - return ppid && String(ppid); // if unique_ppid is undefined return undefined - } - return event.process.parent?.entity_id; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 5a64f3ff9ddb6..20249b81660bb 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../common/types'; -import { extractEventID } from './normalize'; +import { entityId } from '../../../../common/models/event'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; export interface PaginationParams { @@ -15,12 +15,19 @@ export interface PaginationParams { eventID?: string; } +export interface PaginatedResults { + totals: Record<string, number>; + results: ResolverEvent[]; + // content holder for any other extra aggregation counts + extras?: Record<string, Record<string, number>>; +} + interface PaginationCursor { timestamp: number; eventID: string; } -function urlEncodeCursor(data: PaginationCursor) { +function urlEncodeCursor(data: PaginationCursor): string { const value = JSON.stringify(data); return Buffer.from(value, 'utf8') .toString('base64') @@ -56,10 +63,16 @@ export function getPaginationParams(limit: number, after?: string): PaginationPa return { size: limit }; } -export function paginate(pagination: PaginationParams, field: string, query: JsonObject) { +export function paginate( + pagination: PaginationParams, + tiebreaker: string, + aggregator: string, + query: JsonObject +): JsonObject { const { size, timestamp, eventID } = pagination; - query.sort = [{ '@timestamp': 'asc' }, { [field]: 'asc' }]; - query.aggs = { total: { value_count: { field } } }; + query.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + query.aggs = query.aggs || {}; + query.aggs = Object.assign({}, query.aggs, { totals: { terms: { field: aggregator, size } } }); query.size = size; if (timestamp && eventID) { query.search_after = [timestamp, eventID] as Array<number | string>; @@ -67,25 +80,28 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso return query; } -export function paginatedResults( - response: SearchResponse<ResolverEvent> -): { total: number; results: ResolverEvent[]; nextCursor: string | null } { - const total = response.aggregations?.total?.value || 0; - if (response.hits.hits.length === 0) { - return { total, results: [], nextCursor: null }; +export function buildPaginationCursor(total: number, results: ResolverEvent[]): string | null { + if (total > results.length && results.length > 0) { + const lastResult = results[results.length - 1]; + const cursor = { + timestamp: lastResult['@timestamp'], + eventID: entityId(lastResult), + }; + return urlEncodeCursor(cursor); } + return null; +} - const results: ResolverEvent[] = []; - for (const hit of response.hits.hits) { - results.push(hit._source); +export function paginatedResults(response: SearchResponse<ResolverEvent>): PaginatedResults { + if (response.hits.hits.length === 0) { + return { totals: {}, results: [] }; } - // results will be at least 1 because of length check at the top of the function - const next = results[results.length - 1]; - const cursor = { - timestamp: next['@timestamp'], - eventID: extractEventID(next), - }; + const totals = response.aggregations?.totals?.buckets?.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); - return { total, results, nextCursor: urlEncodeCursor(cursor) }; + const results = response.hits.hits.map(hit => hit._source); + return { totals, results }; } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts new file mode 100644 index 0000000000000..5a55c23b90873 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { + ResolverEvent, + ResolverNode, + ResolverNodeStats, + ResolverNodePagination, +} from '../../../../common/types'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { buildPaginationCursor } from './pagination'; + +type ExtractFunction = (event: ResolverEvent) => string | undefined; + +function createNode(id: string): ResolverNode { + return { id, children: [], pagination: {}, events: [], lifecycle: [] }; +} +/** + * This class aids in constructing a tree of process events. It works in the following way: + * + * 1. We construct a tree structure starting with the root node for the event we're requesting. + * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of + * process identifiers that updates the tree structure as we push values into the cache. + * + * When we query a single level of results for child process events we have a flattened, sorted result + * list that we need to add into a constructed tree. We also need to signal in an API response whether + * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent + * process. So, at the end of our tree construction we have a relational layout of the events with no + * pagination information for the given parent nodes. In order to actually construct both the tree and + * insert the pagination information we basically do the following: + * + * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process + * "creation" events, this gives us an estimation of the number of associated children per parent + * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) + * into a second query to get the current state of the process via its "lifecycle" events. + * 3. We construct the tree above with the "lifecycle" events. + * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our + * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set + * of values". + * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected + * number of children to indicate "we have not yet attempted to get any children". + * + * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination + * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand + * the number of documents we can retrieve per level due to the exponential fanout of child processes, + * what this means is that noisy neighbors for a given level may hide other child process events that occur later + * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious + * event doesn't show up in the tree at the beginning. + */ + +export class Tree { + protected cache: Map<string, ResolverNode>; + protected root: ResolverNode; + protected id: string; + + constructor(id: string) { + const root = createNode(id); + this.id = id; + this.cache = new Map(); + this.root = root; + this.cache.set(id, root); + } + + public render(): ResolverNode { + return this.root; + } + + public ids(): string[] { + return [...this.cache.keys()]; + } + + public static async merge( + childrenPromise: Promise<Tree>, + ancestorsPromise: Promise<Tree>, + eventsPromise: Promise<Tree> + ): Promise<Tree> { + const [children, ancestors, events] = await Promise.all([ + childrenPromise, + ancestorsPromise, + eventsPromise, + ]); + + /* + * we only allow for merging when we have partial trees that + * represent the same root node + */ + const rootID = children.id; + if (rootID !== ancestors.id || rootID !== events.id) { + throw new Error('cannot merge trees with different roots'); + } + + Object.entries(ancestors.cache).forEach(([id, node]) => { + if (rootID !== id) { + children.cache.set(id, node); + } + }); + + children.root.lifecycle = ancestors.root.lifecycle; + children.root.ancestors = ancestors.root.ancestors; + children.root.events = events.root.events; + + Object.assign(children.root.pagination, ancestors.root.pagination, events.root.pagination); + + return children; + } + + public addEvent(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.events.push(event); + } + }); + } + + public addAncestor(id: string, ...events: ResolverEvent[]): void { + events.forEach(event => { + const ancestorID = entityId(event); + if (this.cache.get(ancestorID) === undefined) { + const newParent = createNode(ancestorID); + this.cache.set(ancestorID, newParent); + if (!this.root.ancestors) { + this.root.ancestors = []; + } + this.root.ancestors.push(newParent); + } + const currentAncestor = this.cache.get(ancestorID); + if (currentAncestor !== undefined) { + currentAncestor.lifecycle.push(event); + } + }); + } + + public addStats(id: string, stats: ResolverNodeStats): void { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.stats = stats; + } + } + + public setNextAncestor(next: string | null): void { + this.root.pagination.nextAncestor = next; + } + + public setNextEvent(next: string | null): void { + this.root.pagination.nextEvent = next; + } + + public setNextAlert(next: string | null): void { + this.root.pagination.nextAlert = next; + } + + public addChild(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + const parentID = parentEntityId(event); + + this.ensureCache(parentID); + let currentNode = this.cache.get(id); + + if (currentNode === undefined) { + currentNode = createNode(id); + this.cache.set(id, currentNode); + if (parentID !== undefined) { + const parentNode = this.cache.get(parentID); + if (parentNode !== undefined) { + parentNode.children.push(currentNode); + } + } + } + currentNode.lifecycle.push(event); + }); + } + + public markLeafNode(...ids: string[]): void { + ids.forEach(id => { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined && !currentNode.pagination.nextChild) { + currentNode.pagination.nextChild = null; + } + }); + } + + public paginateEvents(totals: Record<string, number>, events: ResolverEvent[]): void { + return this.paginate(entityId, 'nextEvent', totals, events); + } + + public paginateChildren(totals: Record<string, number>, children: ResolverEvent[]): void { + return this.paginate(parentEntityId, 'nextChild', totals, children); + } + + private paginate( + grouper: ExtractFunction, + attribute: keyof ResolverNodePagination, + totals: Record<string, number>, + records: ResolverEvent[] + ): void { + const grouped = _.groupBy(records, grouper); + Object.entries(totals).forEach(([id, total]) => { + if (this.cache.get(id) !== undefined) { + if (grouped[id]) { + /* + * if we have any results, attempt to build a pagination cursor, the function + * below hands back a null value if no cursor is necessary because we have + * all of the records. + */ + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.pagination[attribute] = buildPaginationCursor(total, grouped[id]); + } + } + } + }); + } + + private ensureCache(id: string | undefined): void { + if (id === undefined || this.cache.get(id) === undefined) { + throw new Error('dangling node'); + } + } +} diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 9c1dff60f9727..f487e9262e50e 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 5e93f320c009f..9c923fe77d035 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 de3c9d631fbca..8cc2c74b60e57 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 470123ada48ea..f79962a324131 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 6d5c6b31a637c..47d273b9981e3 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 6d4c9b67abc1b..17c073c4b27f9 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -112,7 +112,7 @@ describe('EventLogStart', () => { { page: 1, per_page: 10, - sort_field: 'event.start', + sort_field: '@timestamp', sort_order: 'asc', } ); @@ -193,7 +193,7 @@ describe('EventLogStart', () => { { page: 1, per_page: 10, - sort_field: 'event.start', + sort_field: '@timestamp', sort_order: 'asc', start, end, diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 765f0895f8e0d..8ef245e60aadc 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -36,6 +36,7 @@ export const findOptionsSchema = schema.object({ end: optionalDateFieldSchema, sort_field: schema.oneOf( [ + schema.literal('@timestamp'), schema.literal('event.start'), schema.literal('event.end'), schema.literal('event.provider'), @@ -44,7 +45,7 @@ export const findOptionsSchema = schema.object({ schema.literal('message'), ], { - defaultValue: 'event.start', + defaultValue: '@timestamp', } ), sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 35ec0bb2bf6ce..fa479b1b06d5e 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/http_api/log_sources/get_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts new file mode 100644 index 0000000000000..bb79b5ebc5290 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { badRequestErrorRT, forbiddenErrorRT, routeTimingMetadataRT } from '../shared'; +import { logSourceConfigurationRT } from './log_source_configuration'; + +/** + * request + */ + +export const getLogSourceConfigurationRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type GetLogSourceConfigurationRequestParams = rt.TypeOf< + typeof getLogSourceConfigurationRequestParamsRT +>; + +/** + * response + */ + +export const getLogSourceConfigurationSuccessResponsePayloadRT = rt.intersection([ + rt.type({ + data: logSourceConfigurationRT, + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationSuccessResponsePayloadRT +>; + +export const getLogSourceConfigurationErrorResponsePayloadRT = rt.union([ + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type GetLogSourceConfigurationErrorReponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationErrorResponsePayloadRT +>; + +export const getLogSourceConfigurationResponsePayloadRT = rt.union([ + getLogSourceConfigurationSuccessResponsePayloadRT, + getLogSourceConfigurationErrorResponsePayloadRT, +]); + +export type GetLogSourceConfigurationReponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts new file mode 100644 index 0000000000000..ae872cee9aa56 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { routeTimingMetadataRT } from '../shared'; +import { + getLogSourceConfigurationPath, + LOG_SOURCE_CONFIGURATION_PATH, +} from './log_source_configuration'; + +export const LOG_SOURCE_STATUS_PATH_SUFFIX = 'status'; +export const LOG_SOURCE_STATUS_PATH = `${LOG_SOURCE_CONFIGURATION_PATH}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; +export const getLogSourceStatusPath = (sourceId: string) => + `${getLogSourceConfigurationPath(sourceId)}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; + +/** + * request + */ + +export const getLogSourceStatusRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type GetLogSourceStatusRequestParams = rt.TypeOf<typeof getLogSourceStatusRequestParamsRT>; + +/** + * response + */ + +const logIndexFieldRT = rt.strict({ + name: rt.string, + type: rt.string, + searchable: rt.boolean, + aggregatable: rt.boolean, +}); + +export type LogIndexField = rt.TypeOf<typeof logIndexFieldRT>; + +const logSourceStatusRT = rt.strict({ + logIndexFields: rt.array(logIndexFieldRT), + logIndexNames: rt.array(rt.string), +}); + +export type LogSourceStatus = rt.TypeOf<typeof logSourceStatusRT>; + +export const getLogSourceStatusSuccessResponsePayloadRT = rt.intersection([ + rt.type({ + data: logSourceStatusRT, + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogSourceStatusSuccessResponsePayload = rt.TypeOf< + typeof getLogSourceStatusSuccessResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/index.ts b/x-pack/plugins/infra/common/http_api/log_sources/index.ts new file mode 100644 index 0000000000000..5fd1b5efec177 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './get_log_source_configuration'; +export * from './get_log_source_status'; +export * from './log_source_configuration'; +export * from './patch_log_source_configuration'; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts new file mode 100644 index 0000000000000..e8bf63843c623 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const LOG_SOURCE_CONFIGURATION_PATH_PREFIX = '/api/infra/log_source_configurations'; +export const LOG_SOURCE_CONFIGURATION_PATH = `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/{sourceId}`; +export const getLogSourceConfigurationPath = (sourceId: string) => + `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/${sourceId}`; + +export const logSourceConfigurationOriginRT = rt.keyof({ + fallback: null, + internal: null, + stored: null, +}); + +export type LogSourceConfigurationOrigin = rt.TypeOf<typeof logSourceConfigurationOriginRT>; + +const logSourceFieldsConfigurationRT = rt.strict({ + timestamp: rt.string, + tiebreaker: rt.string, +}); + +const logSourceCommonColumnConfigurationRT = rt.strict({ + id: rt.string, +}); + +const logSourceTimestampColumnConfigurationRT = rt.strict({ + timestampColumn: logSourceCommonColumnConfigurationRT, +}); + +const logSourceMessageColumnConfigurationRT = rt.strict({ + messageColumn: logSourceCommonColumnConfigurationRT, +}); + +const logSourceFieldColumnConfigurationRT = rt.strict({ + fieldColumn: rt.intersection([ + logSourceCommonColumnConfigurationRT, + rt.strict({ + field: rt.string, + }), + ]), +}); + +const logSourceColumnConfigurationRT = rt.union([ + logSourceTimestampColumnConfigurationRT, + logSourceMessageColumnConfigurationRT, + logSourceFieldColumnConfigurationRT, +]); + +export const logSourceConfigurationPropertiesRT = rt.strict({ + name: rt.string, + description: rt.string, + logAlias: rt.string, + fields: logSourceFieldsConfigurationRT, + logColumns: rt.array(logSourceColumnConfigurationRT), +}); + +export type LogSourceConfigurationProperties = rt.TypeOf<typeof logSourceConfigurationPropertiesRT>; + +export const logSourceConfigurationRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + origin: logSourceConfigurationOriginRT, + configuration: logSourceConfigurationPropertiesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type LogSourceConfiguration = rt.TypeOf<typeof logSourceConfigurationRT>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts new file mode 100644 index 0000000000000..fd312d7429b60 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { badRequestErrorRT, forbiddenErrorRT } from '../shared'; +import { getLogSourceConfigurationSuccessResponsePayloadRT } from './get_log_source_configuration'; +import { logSourceConfigurationPropertiesRT } from './log_source_configuration'; + +/** + * request + */ + +export const patchLogSourceConfigurationRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type PatchLogSourceConfigurationRequestParams = rt.TypeOf< + typeof patchLogSourceConfigurationRequestParamsRT +>; + +const logSourceConfigurationProperiesPatchRT = rt.partial({ + ...logSourceConfigurationPropertiesRT.type.props, + fields: rt.partial(logSourceConfigurationPropertiesRT.type.props.fields.type.props), +}); + +export type LogSourceConfigurationPropertiesPatch = rt.TypeOf< + typeof logSourceConfigurationProperiesPatchRT +>; + +export const patchLogSourceConfigurationRequestBodyRT = rt.type({ + data: logSourceConfigurationProperiesPatchRT, +}); + +export type PatchLogSourceConfigurationRequestBody = rt.TypeOf< + typeof patchLogSourceConfigurationRequestBodyRT +>; + +/** + * response + */ + +export const patchLogSourceConfigurationSuccessResponsePayloadRT = getLogSourceConfigurationSuccessResponsePayloadRT; + +export type PatchLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< + typeof patchLogSourceConfigurationSuccessResponsePayloadRT +>; + +export const patchLogSourceConfigurationResponsePayloadRT = rt.union([ + patchLogSourceConfigurationSuccessResponsePayloadRT, + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type PatchLogSourceConfigurationReponsePayload = rt.TypeOf< + typeof patchLogSourceConfigurationResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/runtime_types.ts b/x-pack/plugins/infra/common/runtime_types.ts index d5b858df38def..a8d5cd8693a3d 100644 --- a/x-pack/plugins/infra/common/runtime_types.ts +++ b/x-pack/plugins/infra/common/runtime_types.ts @@ -9,6 +9,7 @@ import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { Errors, Type } from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; +import { RouteValidationFunction } from 'kibana/server'; type ErrorFactory = (message: string) => Error; @@ -18,8 +19,21 @@ export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => { throw createError(failure(errors).join('\n')); }; -export const decodeOrThrow = <A, O, I>( - runtimeType: Type<A, O, I>, +export const decodeOrThrow = <DecodedValue, EncodedValue, InputValue>( + runtimeType: Type<DecodedValue, EncodedValue, InputValue>, createError: ErrorFactory = createPlainError -) => (inputValue: I) => +) => (inputValue: InputValue) => pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +type ValdidationResult<Value> = ReturnType<RouteValidationFunction<Value>>; + +export const createValidationFunction = <DecodedValue, EncodedValue, InputValue>( + runtimeType: Type<DecodedValue, EncodedValue, InputValue> +): RouteValidationFunction<DecodedValue> => (inputValue, { badRequest, ok }) => + pipe( + runtimeType.decode(inputValue), + fold<Errors, DecodedValue, ValdidationResult<DecodedValue>>( + (errors: Errors) => badRequest(failure(errors).join('\n')), + (result: DecodedValue) => ok(result) + ) + ); diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index cdefffeb35c15..d4d53b81109c6 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/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 98825567cc204..66f64aee24f6e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './input_fields'; export { SourceConfigurationSettings } from './source_configuration_settings'; export { ViewSourceConfigurationButton } from './view_source_configuration_button'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 267abe631c142..b0981f9b3c41f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -8,11 +8,11 @@ import createContainer from 'constate'; import { isString } from 'lodash'; import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { LogEntriesItem } from '../../../common/http_api'; import { UrlStateContainer } from '../../utils/url_state'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; -import { Source } from '../source'; import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item'; -import { LogEntriesItem } from '../../../common/http_api'; +import { useLogSourceContext } from './log_source'; export enum FlyoutVisibility { hidden = 'hidden', @@ -26,7 +26,7 @@ export interface FlyoutOptionsUrlState { } export const useLogFlyout = () => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false); const [flyoutId, setFlyoutId] = useState<string | null>(null); const [flyoutItem, setFlyoutItem] = useState<LogEntriesItem | null>(null); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts new file mode 100644 index 0000000000000..786cb485b38dd --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceConfigurationPath, + getLogSourceConfigurationSuccessResponsePayloadRT, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callFetchLogSourceConfigurationAPI = async (sourceId: string) => { + const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { + method: 'GET', + }); + + return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts new file mode 100644 index 0000000000000..2f1d15ffaf4d3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceStatusPath, + getLogSourceStatusSuccessResponsePayloadRT, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callFetchLogSourceStatusAPI = async (sourceId: string) => { + const response = await npStart.http.fetch(getLogSourceStatusPath(sourceId), { + method: 'GET', + }); + + return decodeOrThrow(getLogSourceStatusSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts new file mode 100644 index 0000000000000..848801ab3c7ce --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceConfigurationPath, + patchLogSourceConfigurationSuccessResponsePayloadRT, + patchLogSourceConfigurationRequestBodyRT, + LogSourceConfigurationPropertiesPatch, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callPatchLogSourceConfigurationAPI = async ( + sourceId: string, + patchedProperties: LogSourceConfigurationPropertiesPatch +) => { + const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { + method: 'PATCH', + body: JSON.stringify( + patchLogSourceConfigurationRequestBodyRT.encode({ + data: patchedProperties, + }) + ), + }); + + return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/index.ts b/x-pack/plugins/infra/public/containers/logs/log_source/index.ts new file mode 100644 index 0000000000000..5d9c6373050a1 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_source'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts new file mode 100644 index 0000000000000..8332018fddf90 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useState, useMemo, useCallback } from 'react'; +import { + LogSourceConfiguration, + LogSourceStatus, + LogSourceConfigurationPropertiesPatch, + LogSourceConfigurationProperties, +} from '../../../../common/http_api/log_sources'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration'; +import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; +import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; + +export { + LogSourceConfiguration, + LogSourceConfigurationProperties, + LogSourceConfigurationPropertiesPatch, + LogSourceStatus, +}; + +export const useLogSource = ({ sourceId }: { sourceId: string }) => { + const [sourceConfiguration, setSourceConfiguration] = useState< + LogSourceConfiguration | undefined + >(undefined); + + const [sourceStatus, setSourceStatus] = useState<LogSourceStatus | undefined>(undefined); + + const [loadSourceConfigurationRequest, loadSourceConfiguration] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callFetchLogSourceConfigurationAPI(sourceId); + }, + onResolve: ({ data }) => { + setSourceConfiguration(data); + }, + }, + [sourceId] + ); + + const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { + return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties); + }, + onResolve: ({ data }) => { + setSourceConfiguration(data); + loadSourceStatus(); + }, + }, + [sourceId] + ); + + const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callFetchLogSourceStatusAPI(sourceId); + }, + onResolve: ({ data }) => { + setSourceStatus(data); + }, + }, + [sourceId] + ); + + const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ + sourceStatus, + ]); + + const derivedIndexPattern = useMemo( + () => ({ + fields: sourceStatus?.logIndexFields ?? [], + title: sourceConfiguration?.configuration.name ?? 'unknown', + }), + [sourceConfiguration, sourceStatus] + ); + + const isLoadingSourceConfiguration = useMemo( + () => loadSourceConfigurationRequest.state === 'pending', + [loadSourceConfigurationRequest.state] + ); + + const isUpdatingSourceConfiguration = useMemo( + () => updateSourceConfigurationRequest.state === 'pending', + [updateSourceConfigurationRequest.state] + ); + + const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [ + loadSourceStatusRequest.state, + ]); + + const isLoading = useMemo( + () => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration, + [isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration] + ); + + const isUninitialized = useMemo( + () => + loadSourceConfigurationRequest.state === 'uninitialized' || + loadSourceStatusRequest.state === 'uninitialized', + [loadSourceConfigurationRequest.state, loadSourceStatusRequest.state] + ); + + const hasFailedLoadingSource = useMemo( + () => loadSourceConfigurationRequest.state === 'rejected', + [loadSourceConfigurationRequest.state] + ); + + const loadSourceFailureMessage = useMemo( + () => + loadSourceConfigurationRequest.state === 'rejected' + ? `${loadSourceConfigurationRequest.value}` + : undefined, + [loadSourceConfigurationRequest] + ); + + const loadSource = useCallback(() => { + return Promise.all([loadSourceConfiguration(), loadSourceStatus()]); + }, [loadSourceConfiguration, loadSourceStatus]); + + const initialize = useCallback(async () => { + if (!isUninitialized) { + return; + } + + return await loadSource(); + }, [isUninitialized, loadSource]); + + return { + derivedIndexPattern, + hasFailedLoadingSource, + initialize, + isLoading, + isLoadingSourceConfiguration, + isLoadingSourceStatus, + isUninitialized, + loadSource, + loadSourceFailureMessage, + loadSourceConfiguration, + loadSourceStatus, + logIndicesExist, + sourceConfiguration, + sourceId, + sourceStatus, + updateSourceConfiguration, + }; +}; + +export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource); diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 14da2b47bcfa2..625a1ead4d930 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -8,10 +8,10 @@ import { useContext } from 'react'; import { useThrottle } from 'react-use'; import { RendererFunction } from '../../../utils/typed_react'; -import { Source } from '../../source'; import { LogSummaryBuckets, useLogSummary } from './log_summary'; import { LogFilterState } from '../log_filter'; import { LogPositionState } from '../log_position'; +import { useLogSourceContext } from '../log_source'; const FETCH_THROTTLE_INTERVAL = 3000; @@ -24,7 +24,7 @@ export const WithSummary = ({ end: number | null; }>; }) => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const { filterQuery } = useContext(LogFilterState.Context); const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index ed1aa9e72ebae..04b472ceb59c8 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -17,7 +17,7 @@ import { import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module'; @@ -25,11 +25,11 @@ import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_m export const LogEntryCategoriesPageContent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, - } = useSourceContext(); + } = useLogSourceContext(); const { hasLogAnalysisCapabilites, @@ -45,7 +45,7 @@ export const LogEntryCategoriesPageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx index 619cea6eda8b7..cecea733b49e4 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module'; export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useSourceContext(); + const { sourceId, sourceConfiguration } = useLogSourceContext(); const spaceId = useKibanaSpaceId(); return ( <LogEntryCategoriesModuleProvider - indexPattern={source ? source.configuration.logAlias : ''} + indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} sourceId={sourceId} spaceId={spaceId} - timestampField={source ? source.configuration.fields.timestamp : ''} + timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''} > {children} </LogEntryCategoriesModuleProvider> diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 2f34e62d8e611..fc07289f02fe7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -17,7 +17,7 @@ import { import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryRateResultsContent } from './page_results_content'; import { LogEntryRateSetupContent } from './page_setup_content'; import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; @@ -25,11 +25,11 @@ import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; export const LogEntryRatePageContent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, - } = useSourceContext(); + } = useLogSourceContext(); const { hasLogAnalysisCapabilites, @@ -45,7 +45,7 @@ export const LogEntryRatePageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index 67c8ea7660a26..e91ef87bdf34a 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; import { LogEntryRateModuleProvider } from './use_log_entry_rate_module'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useSourceContext(); + const { sourceId, sourceConfiguration } = useLogSourceContext(); const spaceId = useKibanaSpaceId(); return ( <LogEntryRateModuleProvider - indexPattern={source ? source.configuration.logAlias : ''} + indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} sourceId={sourceId} spaceId={spaceId} - timestampField={source ? source.configuration.fields.timestamp : ''} + timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''} > {children} </LogEntryRateModuleProvider> diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index dc210406275d8..2974939a83215 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -8,6 +8,7 @@ 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'; @@ -17,6 +18,7 @@ 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'; @@ -28,6 +30,12 @@ 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, diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index 24c1598787a20..d2db5002f4aa2 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -6,15 +6,16 @@ import React from 'react'; import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis'; -import { SourceProvider } from '../../containers/source'; +import { LogSourceProvider } from '../../containers/logs/log_source'; +// import { SourceProvider } from '../../containers/source'; import { useSourceId } from '../../containers/source_id'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { const [sourceId] = useSourceId(); return ( - <SourceProvider sourceId={sourceId}> + <LogSourceProvider sourceId={sourceId}> <LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider> - </SourceProvider> + </LogSourceProvider> ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings.tsx deleted file mode 100644 index faee7a643085a..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/settings.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; - -export const LogsSettingsPage = () => { - const uiCapabilities = useKibana().services.application?.capabilities; - return ( - <SourceConfigurationSettings - shouldAllowEdit={uiCapabilities?.logs?.configureSource as boolean} - displaySettings="logs" - /> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx new file mode 100644 index 0000000000000..6e68debceac70 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiButton, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { euiStyled } from '../../../../../observability/public'; +import { LogColumnConfiguration } from '../../../utils/source_configuration'; +import { useVisibilityState } from '../../../utils/use_visibility_state'; + +interface SelectableColumnOption { + optionProps: EuiSelectableOption; + columnConfiguration: LogColumnConfiguration; +} + +export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ + addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void; + availableFields: string[]; + isDisabled?: boolean; +}> = ({ addLogColumn, availableFields, isDisabled }) => { + const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false); + + const availableColumnOptions = useMemo<SelectableColumnOption[]>( + () => [ + { + optionProps: { + append: <SystemColumnBadge />, + 'data-test-subj': 'addTimestampLogColumn', + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with field names + key: 'timestamp', + label: 'Timestamp', + }, + columnConfiguration: { + timestampColumn: { + id: uuidv4(), + }, + }, + }, + { + optionProps: { + 'data-test-subj': 'addMessageLogColumn', + append: <SystemColumnBadge />, + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with field names + key: 'message', + label: 'Message', + }, + columnConfiguration: { + messageColumn: { + id: uuidv4(), + }, + }, + }, + ...availableFields.map<SelectableColumnOption>(field => ({ + optionProps: { + 'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`, + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with fields that only differ in the + // case (e.g. the metricbeat mongodb module) + key: `field-${field}`, + label: field, + }, + columnConfiguration: { + fieldColumn: { + id: uuidv4(), + field, + }, + }, + })), + ], + [availableFields] + ); + + const availableOptions = useMemo<EuiSelectableOption[]>( + () => availableColumnOptions.map(availableColumnOption => availableColumnOption.optionProps), + [availableColumnOptions] + ); + + const handleColumnSelection = useCallback( + (selectedOptions: EuiSelectableOption[]) => { + closePopover(); + + const selectedOptionIndex = selectedOptions.findIndex( + selectedOption => selectedOption.checked === 'on' + ); + const selectedOption = availableColumnOptions[selectedOptionIndex]; + + addLogColumn(selectedOption.columnConfiguration); + }, + [addLogColumn, availableColumnOptions, closePopover] + ); + + return ( + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButton + data-test-subj="addLogColumnButton" + isDisabled={isDisabled} + iconType="plusInCircle" + onClick={openPopover} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.addLogColumnButtonLabel" + defaultMessage="Add column" + /> + </EuiButton> + } + closePopover={closePopover} + id="addLogColumn" + isOpen={isOpen} + ownFocus + panelPaddingSize="none" + > + <EuiSelectable + height={600} + listProps={selectableListProps} + onChange={handleColumnSelection} + options={availableOptions} + searchable + searchProps={searchProps} + singleSelection + > + {(list, search) => ( + <SelectableContent data-test-subj="addLogColumnPopover"> + <EuiPopoverTitle>{search}</EuiPopoverTitle> + {list} + </SelectableContent> + )} + </EuiSelectable> + </EuiPopover> + ); +}; + +const searchProps = { + 'data-test-subj': 'fieldSearchInput', +}; + +const selectableListProps = { + showIcons: false, +}; + +const SystemColumnBadge: React.FunctionComponent = () => ( + <EuiBadge> + <FormattedMessage + id="xpack.infra.sourceConfiguration.systemColumnBadgeLabel" + defaultMessage="System" + /> + </EuiBadge> +); + +const SelectableContent = euiStyled.div` + width: 400px; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx new file mode 100644 index 0000000000000..ac3b75ad97bb2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiCallOut, + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { InputFieldProps } from '../../../components/source_configuration'; + +interface FieldsConfigurationPanelProps { + isLoading: boolean; + readOnly: boolean; + tiebreakerFieldProps: InputFieldProps; + timestampFieldProps: InputFieldProps; +} + +export const FieldsConfigurationPanel = ({ + isLoading, + readOnly, + tiebreakerFieldProps, + timestampFieldProps, +}: FieldsConfigurationPanelProps) => { + const isTimestampValueDefault = timestampFieldProps.value === '@timestamp'; + const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc'; + + return ( + <EuiForm> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.fieldsSectionTitle" + defaultMessage="Fields" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiCallOut + title={i18n.translate('xpack.infra.sourceConfiguration.deprecationNotice', { + defaultMessage: 'Deprecation Notice', + })} + color="warning" + iconType="help" + > + <p> + <FormattedMessage + id="xpack.infra.sourceConfiguration.deprecationMessage" + defaultMessage="Configuring these fields have been deprecated and will be removed in 8.0.0. This application is designed to work with {ecsLink}, you should adjust your indexing to use the {documentationLink}." + values={{ + documentationLink: ( + <EuiLink + href="https://www.elastic.co/guide/en/infrastructure/guide/7.4/infrastructure-metrics.html" + target="BLANK" + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.documentedFields" + defaultMessage="documented fields" + /> + </EuiLink> + ), + ecsLink: ( + <EuiLink + href="https://www.elastic.co/guide/en/ecs/current/index.html" + target="BLANK" + > + ECS + </EuiLink> + ), + }} + /> + </p> + </EuiCallOut> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldDescription" + defaultMessage="Timestamp used to sort log entries" + /> + } + > + <EuiFormRow + error={timestampFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>@timestamp</EuiCode>, + }} + /> + } + isInvalid={timestampFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" + /> + } + > + <EuiFieldText + fullWidth + disabled={isLoading || isTimestampValueDefault} + readOnly={readOnly} + isLoading={isLoading} + {...timestampFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel" + defaultMessage="Tiebreaker" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldDescription" + defaultMessage="Field used to break ties between two entries with the same timestamp" + /> + } + > + <EuiFormRow + error={tiebreakerFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>_doc</EuiCode>, + }} + /> + } + isInvalid={tiebreakerFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel" + defaultMessage="Tiebreaker" + /> + } + > + <EuiFieldText + fullWidth + disabled={isLoading || isTiebreakerValueDefault} + readOnly={readOnly} + isLoading={isLoading} + {...tiebreakerFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </EuiForm> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index.ts b/x-pack/plugins/infra/public/pages/logs/settings/index.ts new file mode 100644 index 0000000000000..ebdda7ebbd587 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './source_configuration_settings'; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts new file mode 100644 index 0000000000000..a97e38884a5bd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { + createInputFieldProps, + validateInputFieldNotEmpty, +} from '../../../components/source_configuration/input_fields'; + +interface FormState { + name: string; + description: string; + logAlias: string; + tiebreakerField: string; + timestampField: string; +} + +type FormStateChanges = Partial<FormState>; + +export const useLogIndicesConfigurationFormState = ({ + initialFormState = defaultFormState, +}: { + initialFormState?: FormState; +}) => { + const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({}); + + const resetForm = useCallback(() => setFormStateChanges({}), []); + + const formState = useMemo( + () => ({ + ...initialFormState, + ...formStateChanges, + }), + [initialFormState, formStateChanges] + ); + + const nameFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.name), + name: 'name', + onChange: name => setFormStateChanges(changes => ({ ...changes, name })), + value: formState.name, + }), + [formState.name] + ); + const logAliasFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.logAlias), + name: 'logAlias', + onChange: logAlias => setFormStateChanges(changes => ({ ...changes, logAlias })), + value: formState.logAlias, + }), + [formState.logAlias] + ); + const tiebreakerFieldFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.tiebreakerField), + name: `tiebreakerField`, + onChange: tiebreakerField => + setFormStateChanges(changes => ({ ...changes, tiebreakerField })), + value: formState.tiebreakerField, + }), + [formState.tiebreakerField] + ); + const timestampFieldFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.timestampField), + name: `timestampField`, + onChange: timestampField => + setFormStateChanges(changes => ({ ...changes, timestampField })), + value: formState.timestampField, + }), + [formState.timestampField] + ); + + const fieldProps = useMemo( + () => ({ + name: nameFieldProps, + logAlias: logAliasFieldProps, + tiebreakerField: tiebreakerFieldFieldProps, + timestampField: timestampFieldFieldProps, + }), + [nameFieldProps, logAliasFieldProps, tiebreakerFieldFieldProps, timestampFieldFieldProps] + ); + + const errors = useMemo( + () => + Object.values(fieldProps).reduce<ReactNode[]>( + (accumulatedErrors, { error }) => [...accumulatedErrors, ...error], + [] + ), + [fieldProps] + ); + + const isFormValid = useMemo(() => errors.length <= 0, [errors]); + + const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); + + return { + errors, + fieldProps, + formState, + formStateChanges, + isFormDirty, + isFormValid, + resetForm, + }; +}; + +const defaultFormState: FormState = { + name: '', + description: '', + logAlias: '', + tiebreakerField: '', + timestampField: '', +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx new file mode 100644 index 0000000000000..83effaa3d51a5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { InputFieldProps } from '../../../components/source_configuration'; + +interface IndicesConfigurationPanelProps { + isLoading: boolean; + readOnly: boolean; + logAliasFieldProps: InputFieldProps; +} + +export const IndicesConfigurationPanel = ({ + isLoading, + readOnly, + logAliasFieldProps, +}: IndicesConfigurationPanelProps) => ( + <EuiForm> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.indicesSectionTitle" + defaultMessage="Indices" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesTitle" + defaultMessage="Log indices" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesDescription" + defaultMessage="Index pattern for matching indices that contain log data" + /> + } + > + <EuiFormRow + error={logAliasFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>filebeat-*</EuiCode>, + }} + /> + } + isInvalid={logAliasFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesLabel" + defaultMessage="Log indices" + /> + } + > + <EuiFieldText + data-test-subj="logIndicesInput" + fullWidth + disabled={isLoading} + isLoading={isLoading} + readOnly={readOnly} + {...logAliasFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </EuiForm> +); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx new file mode 100644 index 0000000000000..0beccffe5f4e8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { + FieldLogColumnConfiguration, + isMessageLogColumnConfiguration, + isTimestampLogColumnConfiguration, + LogColumnConfiguration, + MessageLogColumnConfiguration, + TimestampLogColumnConfiguration, +} from '../../../utils/source_configuration'; + +export interface TimestampLogColumnConfigurationProps { + logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn']; + remove: () => void; + type: 'timestamp'; +} + +export interface MessageLogColumnConfigurationProps { + logColumnConfiguration: MessageLogColumnConfiguration['messageColumn']; + remove: () => void; + type: 'message'; +} + +export interface FieldLogColumnConfigurationProps { + logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn']; + remove: () => void; + type: 'field'; +} + +export type LogColumnConfigurationProps = + | TimestampLogColumnConfigurationProps + | MessageLogColumnConfigurationProps + | FieldLogColumnConfigurationProps; + +interface FormState { + logColumns: LogColumnConfiguration[]; +} + +type FormStateChanges = Partial<FormState>; + +export const useLogColumnsConfigurationFormState = ({ + initialFormState = defaultFormState, +}: { + initialFormState?: FormState; +}) => { + const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({}); + + const resetForm = useCallback(() => setFormStateChanges({}), []); + + const formState = useMemo( + () => ({ + ...initialFormState, + ...formStateChanges, + }), + [initialFormState, formStateChanges] + ); + + const logColumnConfigurationProps = useMemo<LogColumnConfigurationProps[]>( + () => + formState.logColumns.map( + (logColumn): LogColumnConfigurationProps => { + const remove = () => + setFormStateChanges(changes => ({ + ...changes, + logColumns: formState.logColumns.filter(item => item !== logColumn), + })); + + if (isTimestampLogColumnConfiguration(logColumn)) { + return { + logColumnConfiguration: logColumn.timestampColumn, + remove, + type: 'timestamp', + }; + } else if (isMessageLogColumnConfiguration(logColumn)) { + return { + logColumnConfiguration: logColumn.messageColumn, + remove, + type: 'message', + }; + } else { + return { + logColumnConfiguration: logColumn.fieldColumn, + remove, + type: 'field', + }; + } + } + ), + [formState.logColumns] + ); + + const addLogColumn = useCallback( + (logColumnConfiguration: LogColumnConfiguration) => + setFormStateChanges(changes => ({ + ...changes, + logColumns: [...formState.logColumns, logColumnConfiguration], + })), + [formState.logColumns] + ); + + const moveLogColumn = useCallback( + (sourceIndex, destinationIndex) => { + if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { + const newLogColumns = [...formState.logColumns]; + newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); + setFormStateChanges(changes => ({ + ...changes, + logColumns: newLogColumns, + })); + } + }, + [formState.logColumns] + ); + + const errors = useMemo( + () => + logColumnConfigurationProps.length <= 0 + ? [ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage" + defaultMessage="The log column list must not be empty." + />, + ] + : [], + [logColumnConfigurationProps] + ); + + const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]); + + const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); + + return { + addLogColumn, + moveLogColumn, + errors, + logColumnConfigurationProps, + formState, + formStateChanges, + isFormDirty, + isFormValid, + resetForm, + }; +}; + +const defaultFormState: FormState = { + logColumns: [], +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx new file mode 100644 index 0000000000000..777f611ef33f7 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback } from 'react'; +import { DragHandleProps, DropResult } from '../../../../../observability/public'; +import { LogColumnConfiguration } from '../../../utils/source_configuration'; +import { AddLogColumnButtonAndPopover } from './add_log_column_popover'; +import { + FieldLogColumnConfigurationProps, + LogColumnConfigurationProps, +} from './log_columns_configuration_form_state'; + +interface LogColumnsConfigurationPanelProps { + availableFields: string[]; + isLoading: boolean; + logColumnConfiguration: LogColumnConfigurationProps[]; + addLogColumn: (logColumn: LogColumnConfiguration) => void; + moveLogColumn: (sourceIndex: number, destinationIndex: number) => void; +} + +export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsConfigurationPanelProps> = ({ + addLogColumn, + moveLogColumn, + availableFields, + isLoading, + logColumnConfiguration, +}) => { + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => + destination && moveLogColumn(source.index, destination.index), + [moveLogColumn] + ); + + return ( + <EuiForm> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.logColumnsSectionTitle" + defaultMessage="Log Columns" + /> + </h3> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <AddLogColumnButtonAndPopover + addLogColumn={addLogColumn} + availableFields={availableFields} + isDisabled={isLoading} + /> + </EuiFlexItem> + </EuiFlexGroup> + {logColumnConfiguration.length > 0 ? ( + <EuiDragDropContext onDragEnd={onDragEnd}> + <EuiDroppable droppableId="COLUMN_CONFIG_DROPPABLE_AREA"> + <> + {/* Fragment here necessary for typechecking */} + {logColumnConfiguration.map((column, index) => ( + <EuiDraggable + key={`logColumnConfigurationPanel-${column.logColumnConfiguration.id}`} + index={index} + draggableId={column.logColumnConfiguration.id} + customDragHandle + > + {provided => ( + <LogColumnConfigurationPanel + dragHandleProps={provided.dragHandleProps} + logColumnConfigurationProps={column} + /> + )} + </EuiDraggable> + ))} + </> + </EuiDroppable> + </EuiDragDropContext> + ) : ( + <LogColumnConfigurationEmptyPrompt /> + )} + </EuiForm> + ); +}; + +interface LogColumnConfigurationPanelProps { + logColumnConfigurationProps: LogColumnConfigurationProps; + dragHandleProps: DragHandleProps; +} + +const LogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = props => ( + <> + <EuiSpacer size="m" /> + {props.logColumnConfigurationProps.type === 'timestamp' ? ( + <TimestampLogColumnConfigurationPanel {...props} /> + ) : props.logColumnConfigurationProps.type === 'message' ? ( + <MessageLogColumnConfigurationPanel {...props} /> + ) : ( + <FieldLogColumnConfigurationPanel + logColumnConfigurationProps={props.logColumnConfigurationProps} + dragHandleProps={props.dragHandleProps} + /> + )} + </> +); + +const TimestampLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({ + logColumnConfigurationProps, + dragHandleProps, +}) => ( + <ExplainedLogColumnConfigurationPanel + fieldName="Timestamp" + helpText={ + <FormattedMessage + tagName="span" + id="xpack.infra.sourceConfiguration.timestampLogColumnDescription" + defaultMessage="This system field shows the log entry's time as determined by the {timestampSetting} field setting." + values={{ + timestampSetting: <code>timestamp</code>, + }} + /> + } + removeColumn={logColumnConfigurationProps.remove} + dragHandleProps={dragHandleProps} + /> +); + +const MessageLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({ + logColumnConfigurationProps, + dragHandleProps, +}) => ( + <ExplainedLogColumnConfigurationPanel + fieldName="Message" + helpText={ + <FormattedMessage + tagName="span" + id="xpack.infra.sourceConfiguration.messageLogColumnDescription" + defaultMessage="This system field shows the log entry message as derived from the document fields." + /> + } + removeColumn={logColumnConfigurationProps.remove} + dragHandleProps={dragHandleProps} + /> +); + +const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ + logColumnConfigurationProps: FieldLogColumnConfigurationProps; + dragHandleProps: DragHandleProps; +}> = ({ + logColumnConfigurationProps: { + logColumnConfiguration: { field }, + remove, + }, + dragHandleProps, +}) => { + const fieldLogColumnTitle = i18n.translate( + 'xpack.infra.sourceConfiguration.fieldLogColumnTitle', + { + defaultMessage: 'Field', + } + ); + return ( + <EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <div data-test-subj="moveLogColumnHandle" {...dragHandleProps}> + <EuiIcon type="grab" /> + </div> + </EuiFlexItem> + <EuiFlexItem grow={1}>{fieldLogColumnTitle}</EuiFlexItem> + <EuiFlexItem grow={3}> + <code>{field}</code> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RemoveLogColumnButton + onClick={remove} + columnDescription={`${fieldLogColumnTitle} - ${field}`} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); +}; + +const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ + fieldName: React.ReactNode; + helpText: React.ReactNode; + removeColumn: () => void; + dragHandleProps: DragHandleProps; +}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => ( + <EuiPanel + data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`} + > + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <div data-test-subj="moveLogColumnHandle" {...dragHandleProps}> + <EuiIcon type="grab" /> + </div> + </EuiFlexItem> + <EuiFlexItem grow={1}>{fieldName}</EuiFlexItem> + <EuiFlexItem grow={3}> + <EuiText size="s" color="subdued"> + {helpText} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RemoveLogColumnButton onClick={removeColumn} columnDescription={String(fieldName)} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> +); + +const RemoveLogColumnButton: React.FunctionComponent<{ + onClick?: () => void; + columnDescription: string; +}> = ({ onClick, columnDescription }) => { + const removeColumnLabel = i18n.translate( + 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', + { + defaultMessage: 'Remove {columnDescription} column', + values: { columnDescription }, + } + ); + + return ( + <EuiButtonIcon + color="danger" + data-test-subj="removeLogColumnButton" + iconType="trash" + onClick={onClick} + title={removeColumnLabel} + aria-label={removeColumnLabel} + /> + ); +}; + +const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( + <EuiEmptyPrompt + iconType="list" + title={ + <h2> + <FormattedMessage + id="xpack.infra.sourceConfiguration.noLogColumnsTitle" + defaultMessage="No columns" + /> + </h2> + } + body={ + <p> + <FormattedMessage + id="xpack.infra.sourceConfiguration.noLogColumnsDescription" + defaultMessage="Add a column to this list using the button above." + /> + </p> + } + /> +); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx new file mode 100644 index 0000000000000..92d70955c678f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; +import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source'; +import { useLogIndicesConfigurationFormState } from './indices_configuration_form_state'; +import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state'; + +export const useLogSourceConfigurationFormState = ( + configuration?: LogSourceConfigurationProperties +) => { + const indicesConfigurationFormState = useLogIndicesConfigurationFormState({ + initialFormState: useMemo( + () => + configuration + ? { + name: configuration.name, + description: configuration.description, + logAlias: configuration.logAlias, + tiebreakerField: configuration.fields.tiebreaker, + timestampField: configuration.fields.timestamp, + } + : undefined, + [configuration] + ), + }); + + const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({ + initialFormState: useMemo( + () => + configuration + ? { + logColumns: configuration.logColumns, + } + : undefined, + [configuration] + ), + }); + + const errors = useMemo( + () => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors], + [indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors] + ); + + const resetForm = useCallback(() => { + indicesConfigurationFormState.resetForm(); + logColumnsConfigurationFormState.resetForm(); + }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); + + const isFormDirty = useMemo( + () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, + [indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty] + ); + + const isFormValid = useMemo( + () => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid, + [indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid] + ); + + const formState = useMemo( + () => ({ + name: indicesConfigurationFormState.formState.name, + description: indicesConfigurationFormState.formState.description, + logAlias: indicesConfigurationFormState.formState.logAlias, + fields: { + tiebreaker: indicesConfigurationFormState.formState.tiebreakerField, + timestamp: indicesConfigurationFormState.formState.timestampField, + }, + logColumns: logColumnsConfigurationFormState.formState.logColumns, + }), + [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] + ); + + const formStateChanges = useMemo( + () => ({ + name: indicesConfigurationFormState.formStateChanges.name, + description: indicesConfigurationFormState.formStateChanges.description, + logAlias: indicesConfigurationFormState.formStateChanges.logAlias, + fields: { + tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField, + timestamp: indicesConfigurationFormState.formStateChanges.timestampField, + }, + logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, + }), + [ + indicesConfigurationFormState.formStateChanges, + logColumnsConfigurationFormState.formStateChanges, + ] + ); + + return { + addLogColumn: logColumnsConfigurationFormState.addLogColumn, + moveLogColumn: logColumnsConfigurationFormState.moveLogColumn, + errors, + formState, + formStateChanges, + isFormDirty, + isFormValid, + indicesConfigurationProps: indicesConfigurationFormState.fieldProps, + logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps, + resetForm, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx new file mode 100644 index 0000000000000..88b1441f0ba7c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiPage, + EuiPageBody, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { FieldsConfigurationPanel } from './fields_configuration_panel'; +import { IndicesConfigurationPanel } from './indices_configuration_panel'; +import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel'; +import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; +import { useLogSourceConfigurationFormState } from './source_configuration_form_state'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { Prompt } from '../../../utils/navigation_warning_prompt'; + +export const LogsSettingsPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; + const shouldAllowEdit = uiCapabilities?.logs?.configureSource === true; + + const { + sourceConfiguration: source, + sourceStatus, + isLoading, + isUninitialized, + updateSourceConfiguration, + } = useLogSourceContext(); + + const availableFields = useMemo( + () => sourceStatus?.logIndexFields.map(field => field.name) ?? [], + [sourceStatus] + ); + + const { + addLogColumn, + moveLogColumn, + indicesConfigurationProps, + logColumnConfigurationProps, + errors, + resetForm, + isFormDirty, + isFormValid, + formStateChanges, + } = useLogSourceConfigurationFormState(source?.configuration); + + const persistUpdates = useCallback(async () => { + await updateSourceConfiguration(formStateChanges); + resetForm(); + }, [updateSourceConfiguration, resetForm, formStateChanges]); + + const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ + shouldAllowEdit, + source, + ]); + + if ((isLoading || isUninitialized) && !source) { + return <SourceLoadingPage />; + } + if (!source?.configuration) { + return null; + } + + return ( + <> + <EuiPage> + <EuiPageBody + className="eui-displayBlock" + restrictWidth + data-test-subj="sourceConfigurationContent" + > + <Prompt prompt={isFormDirty ? unsavedFormPromptMessage : undefined} /> + <EuiPanel paddingSize="l"> + <NameConfigurationPanel + isLoading={isLoading} + nameFieldProps={indicesConfigurationProps.name} + readOnly={!isWriteable} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <IndicesConfigurationPanel + isLoading={isLoading} + logAliasFieldProps={indicesConfigurationProps.logAlias} + readOnly={!isWriteable} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <FieldsConfigurationPanel + isLoading={isLoading} + readOnly={!isWriteable} + tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField} + timestampFieldProps={indicesConfigurationProps.timestampField} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <LogColumnsConfigurationPanel + addLogColumn={addLogColumn} + moveLogColumn={moveLogColumn} + availableFields={availableFields} + isLoading={isLoading} + logColumnConfiguration={logColumnConfigurationProps} + /> + </EuiPanel> + {errors.length > 0 ? ( + <> + <EuiCallOut color="danger"> + <ul> + {errors.map((error, errorIndex) => ( + <li key={errorIndex}>{error}</li> + ))} + </ul> + </EuiCallOut> + <EuiSpacer size="m" /> + </> + ) : null} + <EuiSpacer size="m" /> + <EuiFlexGroup> + {isWriteable && ( + <EuiFlexItem> + {isLoading ? ( + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton color="primary" isLoading fill> + Loading + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ) : ( + <> + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="discardSettingsButton" + color="danger" + iconType="cross" + isDisabled={isLoading || !isFormDirty} + onClick={() => { + resetForm(); + }} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel" + defaultMessage="Discard" + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="applySettingsButton" + color="primary" + isDisabled={!isFormDirty || !isFormValid} + fill + onClick={persistUpdates} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.applySettingsButtonLabel" + defaultMessage="Apply" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiPageBody> + </EuiPage> + </> + ); +}; + +const unsavedFormPromptMessage = i18n.translate( + 'xpack.infra.logSourceConfiguration.unsavedFormPromptMessage', + { + defaultMessage: 'Are you sure you want to leave? Changes will be lost', + } +); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index aff0ac27c36f8..712d625052140 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -5,13 +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 { PageViewLogInContext } from './page_view_log_in_context'; -import { useTrackPageview } from '../../../../../observability/public'; export const StreamPage = () => { useTrackPageview({ app: 'infra_logs', path: 'stream' }); @@ -22,7 +20,6 @@ export const StreamPage = () => { <StreamPageHeader /> <StreamPageContent /> </ColumnarPage> - <PageViewLogInContext /> </LogsPageProviders> ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 010a17dae4ebd..40ac5c74a6836 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -7,21 +7,21 @@ import React from 'react'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { useSourceContext } from '../../../containers/source'; import { LogsPageLogsContent } from './page_logs_content'; import { LogsPageNoIndicesContent } from './page_no_indices_content'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; export const StreamPageContent: React.FunctionComponent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, logIndicesExist, - } = useSourceContext(); + } = useLogSourceContext(); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 3208ea2402950..85781c48f9512 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -5,32 +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 { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; +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, @@ -64,6 +62,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { <WithLogTextviewUrlState /> <WithFlyoutOptionsUrlState /> <LogsToolbar /> + <PageViewLogInContext /> {flyoutVisible ? ( <LogEntryFlyout setFilter={applyLogFilterQuery} @@ -77,7 +76,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { loading={isLoading} /> ) : null} - <PageContent key={`${sourceId}-${version}`}> + <PageContent key={`${sourceId}-${sourceConfiguration?.version}`}> <WithStreamItems> {({ currentHighlightKey, @@ -91,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} 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 0341dc7c35b33..428a7d3fdfe4b 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -12,13 +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 /> @@ -29,7 +27,7 @@ const LogFilterStateProvider: React.FC = ({ children }) => { const ViewLogInContextProvider: React.FC = ({ children }) => { const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); if (!startTimestamp || !endTimestamp) { return null; @@ -47,7 +45,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => { }; const LogEntriesStateProvider: React.FC = ({ children }) => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const { startTimestamp, endTimestamp, @@ -89,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, @@ -106,6 +104,13 @@ 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> diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 2f9a76fd47490..9667272eb2417 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -19,13 +19,12 @@ import { LogFlyout } from '../../../containers/logs/log_flyout'; import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; import { LogFilterState } from '../../../containers/logs/log_filter'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { Source } from '../../../containers/source'; import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; import { LogDatepicker } from '../../../components/logging/log_datepicker'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; export const LogsToolbar = () => { - const { createDerivedIndexPattern } = useContext(Source.Context); - const derivedIndexPattern = createDerivedIndexPattern('logs'); + const { derivedIndexPattern } = useLogSourceContext(); const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext( LogViewConfiguration.Context ); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index fdfc16d6a9bef..9e0f7d5035aaf 100644 --- 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 @@ -4,32 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useCallback, useMemo } from 'react'; -import { noop } from 'lodash'; import { - EuiOverlayMask, + EuiFlexGroup, + EuiFlexItem, EuiModal, EuiModalBody, + EuiOverlayMask, EuiText, EuiTextColor, - EuiFlexGroup, - EuiFlexItem, EuiToolTip, } from '@elastic/eui'; -import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; +import { noop } from 'lodash'; +import React, { useCallback, useContext, useMemo } from 'react'; import { LogEntry } from '../../../../common/http_api'; -import { Source } from '../../../containers/source'; -import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; 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 { source } = useContext(Source.Context); + const { sourceConfiguration } = useLogSourceContext(); const { textScale, textWrap } = useContext(LogViewConfiguration.Context); - const columnConfigurations = useMemo(() => (source && source.configuration.logColumns) || [], [ - source, + const columnConfigurations = useMemo(() => sourceConfiguration?.configuration.logColumns ?? [], [ + sourceConfiguration, ]); const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext( ViewLogInContext.Context diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 88b78dfd3e41c..4ed30380dc164 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -29,6 +29,7 @@ import { initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; import { initSourceRoute } from './routes/source'; export const initInfraServer = (libs: InfraBackendLibs) => { @@ -59,4 +60,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initMetricExplorerRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); + initLogSourceConfigurationRoutes(libs); + initLogSourceStatusRoutes(libs); }; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index eda1fbfa5f4ce..eed7d39b8e74a 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -70,6 +70,9 @@ export class KibanaFramework { case 'put': this.router.put(routeConfig, handler); break; + case 'patch': + this.router.patch(routeConfig, handler); + break; } } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 946f1c14bf593..cf691f73bdc2c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -51,17 +51,19 @@ const getCurrentValueFromAggregations = ( const getParsedFilterQuery: ( filterQuery: string | undefined -) => Record<string, any> = filterQuery => { +) => Record<string, any> | Array<Record<string, any>> = filterQuery => { if (!filterQuery) return {}; try { return JSON.parse(filterQuery).bool; } catch (e) { - return { - query_string: { - query: filterQuery, - analyze_wildcard: true, + return [ + { + query_string: { + query: filterQuery, + analyze_wildcard: true, + }, }, - }; + ]; } }; @@ -159,8 +161,12 @@ export const getElasticsearchMetricQuery = ( return { query: { bool: { - filter: [...rangeFilters, ...metricFieldFilters], - ...parsedFilterQuery, + filter: [ + ...rangeFilters, + ...metricFieldFilters, + ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ], + ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, @@ -233,6 +239,7 @@ const getMetric: ( body: searchBody, index, }); + return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { return { '*': undefined }; // Trigger an Error state diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index b6837e5b769a6..ecbc71f4895c7 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -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/routes/log_sources/configuration.ts b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts new file mode 100644 index 0000000000000..0ce594675773c --- /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 0000000000000..5b68152b329ef --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_sources/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './configuration'; +export * from './status'; diff --git a/x-pack/plugins/infra/server/routes/log_sources/status.ts b/x-pack/plugins/infra/server/routes/log_sources/status.ts new file mode 100644 index 0000000000000..cdd053d2bb10a --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_sources/status.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { + getLogSourceStatusRequestParamsRT, + getLogSourceStatusSuccessResponsePayloadRT, + LOG_SOURCE_STATUS_PATH, +} from '../../../common/http_api/log_sources'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { InfraIndexType } from '../../graphql/types'; +import { InfraBackendLibs } from '../../lib/infra_types'; + +export const initLogSourceStatusRoutes = ({ + framework, + sourceStatus, + fields, +}: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'get', + path: LOG_SOURCE_STATUS_PATH, + validate: { + params: createValidationFunction(getLogSourceStatusRequestParamsRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { sourceId } = request.params; + + try { + const logIndexNames = await sourceStatus.getLogIndexNames(requestContext, sourceId); + const logIndexFields = + logIndexNames.length > 0 + ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) + : []; + + return response.ok({ + body: getLogSourceStatusSuccessResponsePayloadRT.encode({ + data: { + logIndexFields, + logIndexNames, + }, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index c5067480fb953..9bc1293799d3c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = { status: AgentConfigStatus.Active, datasources: [], is_default: true, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system]; diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index 17509571f1985..bff799798ff6e 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, DatasourceInput } from '../types'; +import { Datasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockNewDatasource: NewDatasource = { + const mockDatasource: Datasource = { + id: 'some-uuid', name: 'mock-datasource', description: '', config_id: '', @@ -15,18 +16,13 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { output_id: '', namespace: 'default', inputs: [], - }; - - const mockDatasource: Datasource = { - ...mockNewDatasource, - id: 'some-uuid', revision: 1, }; const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, - config: { + vars: { inputVar: { value: 'input-value' }, inputVar2: { value: undefined }, inputVar3: { @@ -40,11 +36,11 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-foo', enabled: true, dataset: 'foo', - config: { + vars: { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, }, - pkg_stream: { + agent_stream: { fooKey: 'fooValue1', fooKey2: ['fooValue2'], }, @@ -53,7 +49,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-bar', enabled: true, dataset: 'bar', - config: { + vars: { barVar: { value: 'bar-value' }, barVar2: { value: [1, 2] }, barVar3: { @@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('uses name for id when id is not provided in case of new datasource', () => { - expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ - id: 'mock-datasource', - name: 'mock-datasource', - namespace: 'default', - enabled: true, - use_output: 'default', - inputs: [], - }); - }); - it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'some-uuid', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 9e09d3fa3153a..620b663451ea3 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; +import { Datasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; export const storedDatasourceToAgentDatasource = ( - datasource: Datasource | NewDatasource + datasource: Datasource ): FullAgentConfigDatasource => { - const { name, namespace, enabled, package: pkg, inputs } = datasource; + const { id, name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: 'id' in datasource ? datasource.id : name, + id: id || name, name, namespace, enabled, @@ -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 5fa7af2dda79a..a977a1a66e059 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => { name: 'mock-package', title: 'Mock package', version: '0.0.0', + latestVersion: '0.0.0', description: 'description', type: 'mock', categories: [], @@ -132,7 +133,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'foo-foo', enabled: true, dataset: 'foo', - config: { 'var-name': { value: 'foo-var-value' } }, + vars: { 'var-name': { value: 'foo-var-value' } }, }, ], }, @@ -144,13 +145,13 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar', enabled: true, dataset: 'bar', - config: { 'var-name': { type: 'text', value: 'bar-var-value' } }, + vars: { 'var-name': { type: 'text', value: 'bar-var-value' } }, }, { id: 'bar-bar2', enabled: true, dataset: 'bar2', - config: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, + vars: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, }, ], }, @@ -205,7 +206,7 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: 'foo-input-var-value' }, 'foo-input2-var-name': { value: 'foo-input2-var-value' }, 'foo-input3-var-name': { value: undefined }, @@ -215,7 +216,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'foo-foo', enabled: true, dataset: 'foo', - config: { + vars: { 'var-name': { value: 'foo-var-value' }, }, }, @@ -224,7 +225,7 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: ['value1', 'value2'] }, 'bar-input2-var-name': { value: 123456 }, }, @@ -233,7 +234,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar', enabled: true, dataset: 'bar', - config: { + vars: { 'var-name': { value: 'bar-var-value' }, }, }, @@ -241,7 +242,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar2', enabled: true, dataset: 'bar2', - config: { + vars: { 'var-name': { value: 'bar2-var-value' }, }, }, @@ -255,7 +256,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'with-disabled-streams-disabled', enabled: false, dataset: 'disabled', - config: { + vars: { 'var-name': { value: [] }, }, }, diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts index fa3479a69e39d..e7a912ddf1741 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts @@ -53,7 +53,7 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas dataset: packageStream.dataset, }; if (packageStream.vars && packageStream.vars.length) { - stream.config = packageStream.vars.reduce(varsReducer, {}); + stream.vars = packageStream.vars.reduce(varsReducer, {}); } return stream; }) @@ -66,7 +66,7 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas }; if (packageInput.vars && packageInput.vars.length) { - input.config = packageInput.vars.reduce(varsReducer, {}); + input.vars = packageInput.vars.reduce(varsReducer, {}); } inputs.push(input); diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 2372caee512af..96121251b133e 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { SavedObjectAttributes } from 'src/core/public'; import { Datasource, DatasourcePackage, @@ -23,9 +21,10 @@ export interface NewAgentConfig { namespace?: string; description?: string; is_default?: boolean; + monitoring_enabled?: Array<'logs' | 'metrics'>; } -export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { +export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; datasources: string[] | Datasource[]; @@ -60,4 +59,12 @@ export interface FullAgentConfig { }; datasources: FullAgentConfigDatasource[]; revision?: number; + settings?: { + monitoring: { + use_output?: string; + enabled: boolean; + metrics: boolean; + logs: boolean; + }; + }; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 48243a12120f9..ca61a93d9be93 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -17,20 +17,29 @@ export interface DatasourceConfigRecordEntry { export type DatasourceConfigRecord = Record<string, DatasourceConfigRecordEntry>; -export interface DatasourceInputStream { +export interface NewDatasourceInputStream { id: string; enabled: boolean; dataset: string; processors?: string[]; config?: DatasourceConfigRecord; - pkg_stream?: any; + vars?: DatasourceConfigRecord; } -export interface DatasourceInput { +export interface DatasourceInputStream extends NewDatasourceInputStream { + agent_stream?: any; +} + +export interface NewDatasourceInput { type: string; enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; + vars?: DatasourceConfigRecord; + streams: NewDatasourceInputStream[]; +} + +export interface DatasourceInput extends Omit<NewDatasourceInput, 'streams'> { streams: DatasourceInputStream[]; } @@ -42,10 +51,11 @@ export interface NewDatasource { enabled: boolean; package?: DatasourcePackage; output_id: string; - inputs: DatasourceInput[]; + inputs: NewDatasourceInput[]; } -export type Datasource = NewDatasource & { +export interface Datasource extends Omit<NewDatasource, 'inputs'> { id: string; + inputs: DatasourceInput[]; revision: number; -}; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index c750aa99204fa..f8779a879a049 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/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx new file mode 100644 index 0000000000000..1e7a14e350229 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + onClose: () => void; +} + +export const AlphaFlyout: React.FunctionComponent<Props> = ({ onClose }) => { + return ( + <EuiFlyout onClose={onClose} size="m" maxWidth={640}> + <EuiFlyoutHeader hasBorder aria-labelledby="AlphaMessagingFlyoutTitle"> + <EuiTitle size="m"> + <h2 id="AlphaMessagingFlyoutTitle"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.flyoutTitle" + defaultMessage="About this release" + /> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiText size="m"> + <p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.introText" + defaultMessage="This release is experimental and is not subject to the support SLA. It is designed for users to test and offer feedback about Ingest + Manager and the new Elastic Agent. It is not intended for use in production environments since certain features may change or go away in a future release." + /> + </p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.feedbackText" + defaultMessage="We encourage you to read our {docsLink} or to ask questions and send feedback in our {forumLink}." + values={{ + docsLink: ( + <EuiLink href="https://ela.st/ingest-manager-docs" external target="_blank"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.docsLink" + defaultMessage="documentation" + /> + </EuiLink> + ), + forumLink: ( + <EuiLink href="https://ela.st/ingest-manager-forum" external target="_blank"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.forumLink" + defaultMessage="Discuss forum" + /> + </EuiLink> + ), + }} + /> + <p /> + + <p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.warningText" + defaultMessage="{note}: you should not store important data with Ingest Manager + since you will have limited visibility to it in a future release. This version uses an + indexing strategy that will be deprecated in a future release and there is no migration + path. Also, licensing for certain features is under consideration and may change in the future. As a result, you may lose access to certain features based on your license + tier." + values={{ + note: ( + <strong> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.warningNote" + defaultMessage="Note" + /> + </strong> + ), + }} + /> + </p> + </EuiText> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + <FormattedMessage + id="xpack.ingestManager.alphaMessging.closeFlyoutLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index 0f3ddee29fa44..5a06a9a879441 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -3,35 +3,45 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { AlphaFlyout } from './alpha_flyout'; const Message = styled(EuiText).attrs(props => ({ color: 'subdued', textAlign: 'center', + size: 's', }))` padding: ${props => props.theme.eui.paddingSizes.m}; `; -export const AlphaMessaging: React.FC<{}> = () => ( - <Message> - <p> - <small> - <strong> +export const AlphaMessaging: React.FC<{}> = () => { + const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState<boolean>(false); + + return ( + <> + <Message> + <p> + <strong> + <FormattedMessage + id="xpack.ingestManager.alphaMessageTitle" + defaultMessage="Experimental" + /> + </strong> + {' – '} <FormattedMessage - id="xpack.ingestManager.alphaMessageTitle" - defaultMessage="Alpha release" - /> - </strong> - {' – '} - <FormattedMessage - id="xpack.ingestManager.alphaMessageDescription" - defaultMessage="Ingest Manager is under active development and is not + id="xpack.ingestManager.alphaMessageDescription" + defaultMessage="Ingest Manager is under active development and is not intended for production purposes." - /> - </small> - </p> - </Message> -); + />{' '} + <EuiLink color="subdued" onClick={() => setIsAlphaFlyoutOpen(true)}> + View more details. + </EuiLink> + </p> + </Message> + {isAlphaFlyoutOpen && <AlphaFlyout onClose={() => setIsAlphaFlyoutOpen(false)} />} + </> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index d16d835f8c701..bed3f994005ad 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/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index 0d19ecd0cb735..e2fc190e158f9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -5,12 +5,18 @@ */ import { sendRequest, useRequest } from './use_request'; import { datasourceRouteService } from '../../services'; -import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; +import { + CreateDatasourceRequest, + CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, +} from '../../types'; import { DeleteDatasourcesRequest, DeleteDatasourcesResponse, GetDatasourcesRequest, GetDatasourcesResponse, + GetOneDatasourceResponse, } from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { @@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { }); }; +export const sendUpdateDatasource = ( + datasourceId: string, + body: UpdateDatasourceRequest['body'] +) => { + return sendRequest<UpdateDatasourceResponse>({ + path: datasourceRouteService.getUpdatePath(datasourceId), + method: 'put', + body: JSON.stringify(body), + }); +}; + export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { return sendRequest<DeleteDatasourcesResponse>({ path: datasourceRouteService.getDeletePath(), @@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) { query, }); } + +export const sendGetOneDatasource = (datasourceId: string) => { + return sendRequest<GetOneDatasourceResponse>({ + path: datasourceRouteService.getInfoPath(datasourceId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts index e4abb4ccd22cb..10d9e03e986e1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useRequest, UseRequestConfig, sendRequest } from './use_request'; +import { + useRequest, + UseRequestConfig, + sendRequest, + useConditionalRequest, + SendConditionalRequestConfig, +} from './use_request'; import { enrollmentAPIKeyRouteService } from '../../services'; import { GetOneEnrollmentAPIKeyResponse, @@ -14,12 +20,12 @@ import { type RequestOptions = Pick<Partial<UseRequestConfig>, 'pollIntervalMs'>; -export function useGetOneEnrollmentAPIKey(keyId: string, options?: RequestOptions) { - return useRequest<GetOneEnrollmentAPIKeyResponse>({ +export function useGetOneEnrollmentAPIKey(keyId: string | undefined) { + return useConditionalRequest<GetOneEnrollmentAPIKeyResponse>({ method: 'get', - path: enrollmentAPIKeyRouteService.getInfoPath(keyId), - ...options, - }); + path: keyId ? enrollmentAPIKeyRouteService.getInfoPath(keyId) : undefined, + shouldSendRequest: !!keyId, + } as SendConditionalRequestConfig); } export function sendGetOneEnrollmentAPIKey(keyId: string, options?: RequestOptions) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts index c63383637e792..fbbc482fb96af 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useState, useEffect } from 'react'; import { HttpSetup } from 'src/core/public'; import { SendRequestConfig, @@ -35,3 +36,68 @@ export const useRequest = <D = any>(config: UseRequestConfig) => { } return _useRequest<D>(httpClient, config); }; + +export type SendConditionalRequestConfig = + | (SendRequestConfig & { shouldSendRequest: true }) + | (Partial<SendRequestConfig> & { shouldSendRequest: false }); + +export const useConditionalRequest = <D = any>(config: SendConditionalRequestConfig) => { + const [state, setState] = useState<{ + error: Error | null; + data: D | null; + isLoading: boolean; + }>({ + error: null, + data: null, + isLoading: false, + }); + + const { path, method, shouldSendRequest, query, body } = config; + + async function sendGetOneEnrollmentAPIKeyRequest() { + if (!config.shouldSendRequest) { + setState({ + data: null, + isLoading: false, + error: null, + }); + return; + } + + try { + setState({ + data: null, + isLoading: true, + error: null, + }); + const res = await sendRequest<D>({ + method: config.method, + path: config.path, + query: config.query, + body: config.body, + }); + if (res.error) { + throw res.error; + } + setState({ + data: res.data, + isLoading: false, + error: null, + }); + return res; + } catch (error) { + setState({ + data: null, + isLoading: false, + error, + }); + } + } + + useEffect(() => { + sendGetOneEnrollmentAPIKeyRequest(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [path, method, shouldSendRequest, JSON.stringify(query), JSON.stringify(body)]); + + return { ...state, sendRequest: sendGetOneEnrollmentAPIKeyRequest }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 0d53ca34a1fef..92c44d86e47c6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -18,6 +18,7 @@ import { EuiText, EuiComboBox, EuiIconTip, + EuiCheckboxGroup, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -30,7 +31,7 @@ interface ValidationResults { const StyledEuiAccordion = styled(EuiAccordion)` .ingest-active-button { - color: ${props => props.theme.eui.euiColorPrimary}}; + color: ${props => props.theme.eui.euiColorPrimary}; } `; @@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent<Props> = ({ )} </EuiFlexItem> </EuiFlexGroup> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiText> + <h4> + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.monitoringLabel" + defaultMessage="Agent monitoring" + /> + </h4> + </EuiText> + <EuiSpacer size="m" /> + <EuiText size="s"> + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.monitoringDescription" + defaultMessage="Collect data about your agents for debugging and tracking performance." + /> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiCheckboxGroup + options={[ + { + id: 'logs', + label: i18n.translate( + 'xpack.ingestManager.agentConfigForm.monitoringLogsFieldLabel', + { defaultMessage: 'Collect agent logs' } + ), + }, + { + id: 'metrics', + label: i18n.translate( + 'xpack.ingestManager.agentConfigForm.monitoringMetricsFieldLabel', + { defaultMessage: 'Collect agent metrics' } + ), + }, + ]} + idToSelectedMap={(agentConfig.monitoring_enabled || []).reduce( + (acc: { logs: boolean; metrics: boolean }, key) => { + acc[key] = true; + return acc; + }, + { logs: false, metrics: false } + )} + onChange={id => { + if (id !== 'logs' && id !== 'metrics') { + return; + } + + const hasLogs = + agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; + + const previousValues = agentConfig.monitoring_enabled || []; + updateAgentConfig({ + monitoring_enabled: hasLogs + ? previousValues.filter(type => type !== id) + : [...previousValues, id], + }); + }} + /> + </EuiFlexItem> + </EuiFlexGroup> </StyledEuiAccordion> </EuiForm> ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx index 0e8763cb2d4c0..36e987d007679 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx @@ -97,7 +97,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ <EuiFlexGroup direction="column" gutterSize="m"> {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.config![varName].value; + const value = datasourceInput.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -105,8 +105,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInput({ - config: { - ...datasourceInput.config, + vars: { + ...datasourceInput.vars, [varName]: { type: varType, value: newValue, @@ -114,7 +114,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} - errors={inputVarsValidationResults.config![varName]} + errors={inputVarsValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> @@ -141,7 +141,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.config![varName].value; + const value = datasourceInput.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -149,8 +149,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInput({ - config: { - ...datasourceInput.config, + vars: { + ...datasourceInput.vars, [varName]: { type: varType, value: newValue, @@ -158,7 +158,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} - errors={inputVarsValidationResults.config![varName]} + errors={inputVarsValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx index 6b0c68ccb7d3f..586fc6b1d4138 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx @@ -158,7 +158,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInputVars={packageInput.vars} datasourceInput={datasourceInput} updateDatasourceInput={updateDatasourceInput} - inputVarsValidationResults={{ config: inputValidationResults.config }} + inputVarsValidationResults={{ vars: inputValidationResults.vars }} forceShowErrors={forceShowErrors} /> <EuiHorizontalRule margin="m" /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx index 43e8f5a2c060d..7e32936a6fffa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx @@ -101,7 +101,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ <EuiFlexGroup direction="column" gutterSize="m"> {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.config![varName].value; + const value = datasourceInputStream.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -109,8 +109,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInputStream({ - config: { - ...datasourceInputStream.config, + vars: { + ...datasourceInputStream.vars, [varName]: { type: varType, value: newValue, @@ -118,7 +118,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} - errors={inputStreamValidationResults.config![varName]} + errors={inputStreamValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> @@ -145,7 +145,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.config![varName].value; + const value = datasourceInputStream.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -153,8 +153,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInputStream({ - config: { - ...datasourceInputStream.config, + vars: { + ...datasourceInputStream.vars, [varName]: { type: varType, value: newValue, @@ -162,7 +162,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} - errors={inputStreamValidationResults.config![varName]} + errors={inputStreamValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 39d882f7fdf65..f1e3fea6a0742 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ <EuiFlexItem> <EuiText> <h1> - <FormattedMessage - id="xpack.ingestManager.createDatasource.pageTitle" - defaultMessage="Add data source" - /> + {from === 'edit' ? ( + <FormattedMessage + id="xpack.ingestManager.editDatasource.pageTitle" + defaultMessage="Edit data source" + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.createDatasource.pageTitle" + defaultMessage="Add data source" + /> + )} </h1> </EuiText> </EuiFlexItem> <EuiFlexItem> <EuiSpacer size="s" /> <EuiText color="subdued" size="s"> - {from === 'config' ? ( + {from === 'edit' ? ( + <FormattedMessage + id="xpack.ingestManager.editDatasource.pageDescription" + defaultMessage="Follow the instructions below to edit this data source." + /> + ) : from === 'config' ? ( <FormattedMessage id="xpack.ingestManager.createDatasource.pageDescriptionfromConfig" defaultMessage="Follow the instructions below to add an integration to this agent configuration." @@ -68,7 +80,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ <EuiFlexGroup justifyContent="flexEnd" direction={'row'} gutterSize="xl"> <EuiFlexItem grow={false}> <EuiSpacer size="s" /> - {agentConfig && from === 'config' ? ( + {agentConfig && (from === 'config' || from === 'edit') ? ( <EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse"> <EuiDescriptionListTitle> <FormattedMessage diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 3c79fe17fdbb5..8e7042c1275ad 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -28,7 +28,7 @@ import { } from '../../../hooks'; import { useLinks as useEPMLinks } from '../../epm/hooks'; import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components'; -import { CreateDatasourceFrom } from './types'; +import { CreateDatasourceFrom, DatasourceFormState } from './types'; import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; @@ -85,6 +85,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); + setFormState('VALID'); } else { setFormState('INVALID'); setPackageInfo(undefined); @@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; // Save datasource - const [formState, setFormState] = useState< - 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED' - >('INVALID'); + const [formState, setFormState] = useState<DatasourceFormState>('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { error } = await saveDatasource(); if (!error) { history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { + defaultMessage: `Successfully added '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); } else { notifications.toasts.addError(error, { title: 'Error', @@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} /> ) : null, }, @@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { children: agentConfig && packageInfo ? ( <StepConfigureDatasource - agentConfig={agentConfig} packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts index a45fabeb5ed6a..b970a7d222001 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts @@ -120,7 +120,7 @@ describe('Ingest Manager - validateDatasource()', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, 'foo-input3-var-name': { value: ['test'], type: 'text' }, @@ -130,14 +130,14 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'foo-foo', dataset: 'foo', enabled: true, - config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, ], }, { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, 'bar-input2-var-name': { value: 'test', type: 'text' }, }, @@ -146,13 +146,13 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'bar-bar', dataset: 'bar', enabled: true, - config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, { id: 'bar-bar2', dataset: 'bar2', enabled: true, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, ], }, @@ -169,7 +169,7 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'with-disabled-streams-disabled', dataset: 'disabled', enabled: false, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, { id: 'with-disabled-streams-disabled2', @@ -188,7 +188,7 @@ describe('Ingest Manager - validateDatasource()', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: undefined, type: 'text' }, 'foo-input2-var-name': { value: '', type: 'text' }, 'foo-input3-var-name': { value: [], type: 'text' }, @@ -198,14 +198,14 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'foo-foo', dataset: 'foo', enabled: true, - config: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, + vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, }, ], }, { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, 'bar-input2-var-name': { value: undefined, type: 'text' }, }, @@ -214,13 +214,13 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'bar-bar', dataset: 'bar', enabled: true, - config: { 'var-name': { value: ' \n\n', type: 'yaml' } }, + vars: { 'var-name': { value: ' \n\n', type: 'yaml' } }, }, { id: 'bar-bar2', dataset: 'bar2', enabled: true, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, ], }, @@ -237,7 +237,7 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'with-disabled-streams-disabled', dataset: 'disabled', enabled: false, - config: { + vars: { 'var-name': { value: 'invalid value but not checked due to not enabled', type: 'text', @@ -259,22 +259,22 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': null, 'foo-input3-var-name': null, }, - streams: { 'foo-foo': { config: { 'var-name': null } } }, + streams: { 'foo-foo': { vars: { 'var-name': null } } }, }, bar: { - config: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, + vars: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, streams: { - 'bar-bar': { config: { 'var-name': null } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': null } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }; @@ -289,25 +289,25 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': ['foo-input2-var-name is required'], 'foo-input3-var-name': ['foo-input3-var-name is required'], }, - streams: { 'foo-foo': { config: { 'var-name': ['Invalid YAML format'] } } }, + streams: { 'foo-foo': { vars: { 'var-name': ['Invalid YAML format'] } } }, }, bar: { - config: { + vars: { 'bar-input-var-name': ['Invalid format'], 'bar-input2-var-name': ['bar-input2-var-name is required'], }, streams: { - 'bar-bar': { config: { 'var-name': ['var-name is required'] } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': ['var-name is required'] } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }); @@ -336,25 +336,25 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': ['foo-input2-var-name is required'], 'foo-input3-var-name': ['foo-input3-var-name is required'], }, - streams: { 'foo-foo': { config: { 'var-name': null } } }, + streams: { 'foo-foo': { vars: { 'var-name': null } } }, }, bar: { - config: { + vars: { 'bar-input-var-name': ['Invalid format'], 'bar-input2-var-name': ['bar-input2-var-name is required'], }, streams: { - 'bar-bar': { config: { 'var-name': null } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': null } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }); @@ -411,7 +411,7 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns true for stream validation results with errors', () => { expect( validationHasErrors({ - config: { foo: ['foo error'], bar: null }, + vars: { foo: ['foo error'], bar: null }, }) ).toBe(true); }); @@ -419,7 +419,7 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns false for stream validation results with no errors', () => { expect( validationHasErrors({ - config: { foo: null, bar: null }, + vars: { foo: null, bar: null }, }) ).toBe(false); }); @@ -427,14 +427,14 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns true for input validation results with errors', () => { expect( validationHasErrors({ - config: { foo: ['foo error'], bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }) ).toBe(true); expect( validationHasErrors({ - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, }) ).toBe(true); }); @@ -442,8 +442,8 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns false for input validation results with no errors', () => { expect( validationHasErrors({ - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }) ).toBe(false); }); @@ -455,8 +455,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) @@ -467,8 +467,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: ['foo error'], bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) @@ -479,8 +479,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, }, }, }) @@ -494,8 +494,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts index 518e2bfc1af07..3a712b072dac1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts @@ -21,7 +21,7 @@ type Errors = string[] | null; type ValidationEntry = Record<string, Errors>; export interface DatasourceConfigValidationResults { - config?: ValidationEntry; + vars?: ValidationEntry; } export type DatasourceInputValidationResults = DatasourceConfigValidationResults & { @@ -77,12 +77,12 @@ export const validateDatasource = ( // Validate each datasource input with either its own config fields or streams datasource.inputs.forEach(input => { - if (!input.config && !input.streams) { + if (!input.vars && !input.streams) { return; } const inputValidationResults: DatasourceInputValidationResults = { - config: undefined, + vars: undefined, streams: {}, }; @@ -95,27 +95,27 @@ export const validateDatasource = ( ); // Validate input-level config fields - const inputConfigs = Object.entries(input.config || {}); + const inputConfigs = Object.entries(input.vars || {}); if (inputConfigs.length) { - inputValidationResults.config = inputConfigs.reduce((results, [name, configEntry]) => { + inputValidationResults.vars = inputConfigs.reduce((results, [name, configEntry]) => { results[name] = input.enabled ? validateDatasourceConfig(configEntry, inputVarsByName[name]) : null; return results; }, {} as ValidationEntry); } else { - delete inputValidationResults.config; + delete inputValidationResults.vars; } // Validate each input stream with config fields if (input.streams.length) { input.streams.forEach(stream => { - if (!stream.config) { + if (!stream.vars) { return; } const streamValidationResults: DatasourceConfigValidationResults = { - config: undefined, + vars: undefined, }; const streamVarsByName = ( @@ -130,7 +130,7 @@ export const validateDatasource = ( }, {} as Record<string, RegistryVarsEntry>); // Validate stream-level config fields - streamValidationResults.config = Object.entries(stream.config).reduce( + streamValidationResults.vars = Object.entries(stream.vars).reduce( (results, [name, configEntry]) => { results[name] = input.enabled && stream.enabled @@ -147,7 +147,7 @@ export const validateDatasource = ( delete inputValidationResults.streams; } - if (inputValidationResults.config || inputValidationResults.streams) { + if (inputValidationResults.vars || inputValidationResults.streams) { validationResults.inputs![input.type] = inputValidationResults; } }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index 843891b63ca01..118c7e30f13f4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, @@ -15,75 +15,21 @@ import { EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - AgentConfig, - PackageInfo, - Datasource, - NewDatasource, - DatasourceInput, -} from '../../../types'; +import { PackageInfo, NewDatasource, DatasourceInput } from '../../../types'; import { Loading } from '../../../components'; -import { packageToConfigDatasourceInputs } from '../../../services'; import { DatasourceValidationResults, validationHasErrors } from './services'; import { DatasourceInputPanel } from './components'; export const StepConfigureDatasource: React.FunctionComponent<{ - agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial<NewDatasource>) => void; validationResults: DatasourceValidationResults; submitAttempted: boolean; -}> = ({ - agentConfig, - packageInfo, - datasource, - updateDatasource, - validationResults, - submitAttempted, -}) => { - // Form show/hide states - +}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource's package and config info - useEffect(() => { - const dsPackage = datasource.package; - const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; - - // If package has changed, create shell datasource with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name - const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) - .filter(ds => Boolean(ds.name.match(dsPackageNamePattern))) - .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) - .sort(); - - updateDatasource({ - name: `${packageInfo.name}-${ - dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 - }`, - package: { - name: packageInfo.name, - title: packageInfo.title, - version: packageInfo.version, - }, - inputs: packageToConfigDatasourceInputs(packageInfo), - }); - } - - // If agent config has changed, update datasource's config ID and namespace - if (datasource.config_id !== agentConfig.id) { - updateDatasource({ - config_id: agentConfig.id, - namespace: agentConfig.namespace, - }); - } - }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - - // Step B, configure inputs (and their streams) + // Configure inputs (and their streams) // Assume packages only export one datasource for now const renderConfigureInputs = () => packageInfo.datasources && diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx index 792389381eaf0..c4d602c2c2081 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -17,13 +17,16 @@ import { } from '@elastic/eui'; import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; import { packageToConfigDatasourceInputs } from '../../../services'; +import { Loading } from '../../../components'; +import { DatasourceValidationResults } from './services'; export const StepDefineDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial<NewDatasource>) => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + validationResults: DatasourceValidationResults; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false); @@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - return ( + return validationResults ? ( <> <EuiFlexGrid columns={2}> <EuiFlexItem> <EuiFormRow + isInvalid={!!validationResults.name} + error={validationResults.name} label={ <FormattedMessage id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel" @@ -102,6 +107,8 @@ export const StepDefineDatasource: React.FunctionComponent<{ /> </EuiText> } + isInvalid={!!validationResults.description} + error={validationResults.description} > <EuiFieldText value={datasource.description} @@ -161,5 +168,7 @@ export const StepDefineDatasource: React.FunctionComponent<{ </Fragment> ) : null} </> + ) : ( + <Loading /> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts index 85cc758fc4c46..10b30a5696d83 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config'; +export type CreateDatasourceFrom = 'package' | 'config' | 'edit'; +export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 1eee9f6b0c346..a0418c5f256c4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -19,7 +19,7 @@ import { import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; -import { useCapabilities } from '../../../../../hooks'; +import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; @@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ }) => { const hasWriteCapabilities = useCapabilities().write; const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); + const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ <TableRowActions items={[ // FIXME: implement View datasource action + // <EuiContextMenuItem + // disabled + // icon="inspect" + // onClick={() => {}} + // key="datasourceView" + // > + // <FormattedMessage + // id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle" + // defaultMessage="View data source" + // /> + // </EuiContextMenuItem>, <EuiContextMenuItem - disabled - icon="inspect" - onClick={() => {}} - key="datasourceView" - > - <FormattedMessage - id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle" - defaultMessage="View data source" - /> - </EuiContextMenuItem>, - // FIXME: implement Edit datasource action - <EuiContextMenuItem - disabled + disabled={!hasWriteCapabilities} icon="pencil" - onClick={() => {}} + href={`${editDatasourceLink}/${datasource.id}`} key="datasourceEdit" > <FormattedMessage @@ -225,12 +225,12 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ /> </EuiContextMenuItem>, // FIXME: implement Copy datasource action - <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy"> - <FormattedMessage - id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle" - defaultMessage="Copy data source" - /> - </EuiContextMenuItem>, + // <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy"> + // <FormattedMessage + // id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle" + // defaultMessage="Copy data source" + // /> + // </EuiContextMenuItem>, <DatasourceDeleteProvider agentConfig={config} key="datasourceDelete"> {deleteDatasourcePrompt => { return ( @@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ ], }, ], - [config, hasWriteCapabilities, refreshConfig] + [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] ); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index ad27c590d5eaa..f1d7bd5dbc039 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/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx new file mode 100644 index 0000000000000..d4c39f21a1ea6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect } from 'react'; +import { useRouteMatch, useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiButton, + EuiSteps, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { + useLink, + useCore, + useConfig, + sendUpdateDatasource, + sendGetAgentStatus, + sendGetOneAgentConfig, + sendGetOneDatasource, + sendGetPackageInfoByKey, +} from '../../../hooks'; +import { Loading, Error } from '../../../components'; +import { + CreateDatasourcePageLayout, + ConfirmCreateDatasourceModal, +} from '../create_datasource_page/components'; +import { + DatasourceValidationResults, + validateDatasource, + validationHasErrors, +} from '../create_datasource_page/services'; +import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types'; +import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource'; +import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource'; + +export const EditDatasourcePage: React.FunctionComponent = () => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const { + params: { configId, datasourceId }, + } = useRouteMatch(); + const history = useHistory(); + + // Agent config, package info, and datasource states + const [isLoadingData, setIsLoadingData] = useState<boolean>(true); + const [loadingError, setLoadingError] = useState<Error>(); + const [agentConfig, setAgentConfig] = useState<AgentConfig>(); + const [packageInfo, setPackageInfo] = useState<PackageInfo>(); + const [datasource, setDatasource] = useState<NewDatasource>({ + name: '', + description: '', + config_id: '', + enabled: true, + output_id: '', + inputs: [], + }); + + // Retrieve agent config, package, and datasource info + useEffect(() => { + const getData = async () => { + setIsLoadingData(true); + setLoadingError(undefined); + try { + const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([ + sendGetOneAgentConfig(configId), + sendGetOneDatasource(datasourceId), + ]); + if (agentConfigData?.item) { + setAgentConfig(agentConfigData.item); + } + if (datasourceData?.item) { + const { id, revision, inputs, ...restOfDatasource } = datasourceData.item; + // Remove `agent_stream` from all stream info, we assign this after saving + const newDatasource = { + ...restOfDatasource, + inputs: inputs.map(input => { + const { streams, ...restOfInput } = input; + return { + ...restOfInput, + streams: streams.map(stream => { + const { agent_stream, ...restOfStream } = stream; + return restOfStream; + }), + }; + }), + }; + setDatasource(newDatasource); + if (datasourceData.item.package) { + const { data: packageData } = await sendGetPackageInfoByKey( + `${datasourceData.item.package.name}-${datasourceData.item.package.version}` + ); + if (packageData?.response) { + setPackageInfo(packageData.response); + setValidationResults(validateDatasource(newDatasource, packageData.response)); + setFormState('VALID'); + } + } + } + } catch (e) { + setLoadingError(e); + } + setIsLoadingData(false); + }; + getData(); + }, [configId, datasourceId]); + + // Retrieve agent count + const [agentCount, setAgentCount] = useState<number>(0); + useEffect(() => { + const getAgentCount = async () => { + const { data } = await sendGetAgentStatus({ configId }); + if (data?.results.total) { + setAgentCount(data.results.total); + } + }; + + if (isFleetEnabled) { + getAgentCount(); + } + }, [configId, isFleetEnabled]); + + // Datasource validation state + const [validationResults, setValidationResults] = useState<DatasourceValidationResults>(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + // Update datasource method + const updateDatasource = (updatedFields: Partial<NewDatasource>) => { + const newDatasource = { + ...datasource, + ...updatedFields, + }; + setDatasource(newDatasource); + + // eslint-disable-next-line no-console + console.debug('Datasource updated', newDatasource); + const newValidationResults = updateDatasourceValidation(newDatasource); + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + if (!hasValidationErrors) { + setFormState('VALID'); + } + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + + return newValidationResult; + } + }; + + // Cancel url + const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + const cancelUrl = CONFIG_URL; + + // Save datasource + const [formState, setFormState] = useState<DatasourceFormState>('INVALID'); + const saveDatasource = async () => { + setFormState('LOADING'); + const result = await sendUpdateDatasource(datasourceId, datasource); + setFormState('SUBMITTED'); + return result; + }; + + const onSubmit = async () => { + if (formState === 'VALID' && hasErrors) { + setFormState('INVALID'); + return; + } + if (agentCount !== 0 && formState !== 'CONFIRM') { + setFormState('CONFIRM'); + return; + } + const { error } = await saveDatasource(); + if (!error) { + history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { + defaultMessage: `Successfully updated '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); + } else { + notifications.toasts.addError(error, { + title: 'Error', + }); + setFormState('VALID'); + } + }; + + const layoutProps = { + from: 'edit' as CreateDatasourceFrom, + cancelUrl, + agentConfig, + packageInfo, + }; + + return ( + <CreateDatasourcePageLayout {...layoutProps}> + {isLoadingData ? ( + <Loading /> + ) : loadingError || !agentConfig || !packageInfo ? ( + <Error + title={ + <FormattedMessage + id="xpack.ingestManager.editDatasource.errorLoadingDataTitle" + defaultMessage="Error loading data" + /> + } + error={ + loadingError || + i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this data source information', + }) + } + /> + ) : ( + <> + {formState === 'CONFIRM' && ( + <ConfirmCreateDatasourceModal + agentCount={agentCount} + agentConfig={agentConfig} + onConfirm={onSubmit} + onCancel={() => setFormState('VALID')} + /> + )} + <EuiSteps + steps={[ + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepDefineDatasourceTitle', + { + defaultMessage: 'Define your data source', + } + ), + children: ( + <StepDefineDatasource + agentConfig={agentConfig} + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} + validationResults={validationResults!} + /> + ), + }, + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), + children: ( + <StepConfigureDatasource + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} + validationResults={validationResults!} + submitAttempted={formState === 'INVALID'} + /> + ), + }, + ]} + /> + <EuiSpacer size="l" /> + <EuiBottomBar css={{ zIndex: 5 }} paddingSize="s"> + <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty color="ghost" href={cancelUrl}> + <FormattedMessage + id="xpack.ingestManager.editDatasource.cancelButton" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + onClick={onSubmit} + isLoading={formState === 'LOADING'} + disabled={formState !== 'VALID'} + iconType="save" + color="primary" + fill + > + <FormattedMessage + id="xpack.ingestManager.editDatasource.saveButton" + defaultMessage="Save data source" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiBottomBar> + </> + )} + </CreateDatasourcePageLayout> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index 71ada155373bf..ef88aa5d17f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; +import { EditDatasourcePage } from './edit_datasource_page'; export const AgentConfigApp: React.FunctionComponent = () => ( <Router> <Switch> + <Route path="/configs/:configId/edit-datasource/:datasourceId"> + <EditDatasourcePage /> + </Route> <Route path="/configs/:configId/add-datasource"> <CreateDatasourcePage /> </Route> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index 1fe116ef36090..9f582e7e2fbe6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent<Props> = ({ onClos description: '', namespace: '', is_default: undefined, + monitoring_enabled: ['logs', 'metrics'], }); const [isLoading, setIsLoading] = useState<boolean>(false); const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 1ea162252c741..3dcc19bc4a5ae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -36,13 +36,11 @@ import { useConfig, useUrlParams, } from '../../../hooks'; -import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; -import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( defaultMessage="Create data source" /> </EuiContextMenuItem>, - - <EuiContextMenuItem disabled={true} icon="copy" key="copyConfig"> - <FormattedMessage - id="xpack.ingestManager.agentConfigList.copyConfigActionText" - defaultMessage="Copy configuration" - /> - </EuiContextMenuItem>, - - <AgentConfigDeleteProvider key="deleteConfig"> - {deleteAgentConfigsPrompt => { - return ( - <DangerEuiContextMenuItem - icon="trash" - disabled={Boolean(config.is_default)} - onClick={() => deleteAgentConfigsPrompt([config.id], onDelete)} - > - <FormattedMessage - id="xpack.ingestManager.agentConfigList.deleteConfigActionText" - defaultMessage="Delete Configuration" - /> - </DangerEuiContextMenuItem> - ); - }} - </AgentConfigDeleteProvider>, + // <EuiContextMenuItem disabled={true} icon="copy" key="copyConfig"> + // <FormattedMessage + // id="xpack.ingestManager.agentConfigList.copyConfigActionText" + // defaultMessage="Copy configuration" + // /> + // </EuiContextMenuItem>, ]} /> ); @@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgentConfigs, setSelectedAgentConfigs] = useState<AgentConfig[]>([]); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> ) : null} <EuiFlexGroup alignItems={'center'} gutterSize="m"> - {selectedAgentConfigs.length ? ( - <EuiFlexItem> - <AgentConfigDeleteProvider> - {deleteAgentConfigsPrompt => ( - <EuiButton - color="danger" - onClick={() => { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); - }} - > - <FormattedMessage - id="xpack.ingestManager.agentConfigList.deleteButton" - defaultMessage="Delete {count, plural, one {# agent config} other {# agent configs}}" - values={{ - count: selectedAgentConfigs.length, - }} - /> - </EuiButton> - )} - </AgentConfigDeleteProvider> - </EuiFlexItem> - ) : null} <EuiFlexItem grow={4}> <SearchBar value={search} @@ -405,13 +356,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { items={agentConfigData ? agentConfigData.items : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agentConfig: AgentConfig) => !agentConfig.is_default, - onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { - setSelectedAgentConfigs(newSelectedAgentConfigs); - }, - }} + isSelectable={false} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx new file mode 100644 index 0000000000000..64223efefaab8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +export const StyledAlert = styled(EuiIcon)` + color: ${props => props.theme.eui.euiColorWarning}; + padding: 0 5px; +`; + +export const UpdateIcon = () => <StyledAlert type="alert" size="l" />; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx index 8ad081cbbabe4..ab7e87b3ad06c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -30,9 +30,15 @@ export function PackageCard({ showInstalledBadge, status, icons, + ...restProps }: PackageCardProps) { const { toDetailView } = useLinks(); - const url = toDetailView({ name, version }); + let urlVersion = version; + // if this is an installed package, link to the version installed + if ('savedObject' in restProps) { + urlVersion = restProps.savedObject.attributes.version || version; + } + const url = toDetailView({ name, version: urlVersion }); return ( <Card diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index 685199245df18..54cb5171f5a3e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -21,6 +21,7 @@ export const AssetTitleMap: Record<AssetType, string> = { 'ingest-pipeline': 'Ingest Pipeline', 'index-pattern': 'Index Pattern', 'index-template': 'Index Template', + 'component-template': 'Component Template', search: 'Saved Search', visualization: 'Visualization', input: 'Agent input', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 0c5f45cdc47a7..244a9a2c7426e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -11,6 +11,7 @@ import { NotificationsStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; +import { useLinks } from '.'; import { InstallStatus } from '../../../types'; interface PackagesInstall { @@ -19,31 +20,55 @@ interface PackagesInstall { interface PackageInstallItem { status: InstallStatus; + version: string | null; } -type InstallPackageProps = Pick<PackageInfo, 'name' | 'version' | 'title'>; +type InstallPackageProps = Pick<PackageInfo, 'name' | 'version' | 'title'> & { + fromUpdate?: boolean; +}; +type SetPackageInstallStatusProps = Pick<PackageInfo, 'name'> & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { + const { toDetailView } = useLinks(); const [packages, setPackage] = useState<PackagesInstall>({}); const setPackageInstallStatus = useCallback( - ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => { + ({ name, status, version }: SetPackageInstallStatusProps) => { + const packageProps: PackageInstallItem = { + status, + version, + }; setPackage((prev: PackagesInstall) => ({ ...prev, - [name]: { status }, + [name]: packageProps, })); }, [] ); + const getPackageInstallStatus = useCallback( + (pkg: string): PackageInstallItem => { + return packages[pkg]; + }, + [packages] + ); + const installPackage = useCallback( - async ({ name, version, title }: InstallPackageProps) => { - setPackageInstallStatus({ name, status: InstallStatus.installing }); + async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => { + const currStatus = getPackageInstallStatus(name); + const newStatus = { ...currStatus, name, status: InstallStatus.installing }; + setPackageInstallStatus(newStatus); const pkgkey = `${name}-${version}`; const res = await sendInstallPackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + if (fromUpdate) { + // if there is an error during update, set it back to the previous version + // as handling of bad update is not implemented yet + setPackageInstallStatus({ ...currStatus, name }); + } else { + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version }); + } notifications.toasts.addWarning({ title: toMountPoint( <FormattedMessage @@ -61,8 +86,15 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar iconType: 'alert', }); } else { - setPackageInstallStatus({ name, status: InstallStatus.installed }); - + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); + if (fromUpdate) { + const settingsUrl = toDetailView({ + name, + version, + panel: 'settings', + }); + window.location.href = settingsUrl; + } notifications.toasts.addSuccess({ title: toMountPoint( <FormattedMessage @@ -81,24 +113,17 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); } }, - [notifications.toasts, setPackageInstallStatus] - ); - - const getPackageInstallStatus = useCallback( - (pkg: string): InstallStatus => { - return packages[pkg].status; - }, - [packages] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] ); const uninstallPackage = useCallback( async ({ name, version, title }: Pick<PackageInfo, 'name' | 'version' | 'title'>) => { - setPackageInstallStatus({ name, status: InstallStatus.uninstalling }); + setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; const res = await sendRemovePackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.installed }); + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); notifications.toasts.addWarning({ title: toMountPoint( <FormattedMessage @@ -116,7 +141,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar iconType: 'alert', }); } else { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version: null }); notifications.toasts.addSuccess({ title: toMountPoint( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index a3d24e7806f34..96aebb08e0c63 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -50,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 fa3245aec02c5..c82b7ed2297a7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - if (packageInstallStatus !== InstallStatus.installed) + if (packageInstallStatus.status !== InstallStatus.installed) return ( <Redirect to={toDetailView({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index d83910f29f1a7..d20350c5db631 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 3239d7b90e3c3..1f3eb2cc9362e 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 cbbf1ce53c4ea..cdad67fd87548 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version'> & { - disabled: boolean; + disabled?: boolean; + isUpdate?: boolean; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true } = props; + const { assets, name, title, version, disabled = true, isUpdate = false } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); - const installationStatus = getPackageInstallStatus(name); + const { status: installationStatus } = getPackageInstallStatus(name); const isInstalling = installationStatus === InstallStatus.installing; const isRemoving = installationStatus === InstallStatus.uninstalling; const isInstalled = installationStatus === InstallStatus.installed; + const showUninstallButton = isInstalled || isRemoving; const [isModalVisible, setModalVisible] = useState<boolean>(false); const toggleModal = useCallback(() => { setModalVisible(!isModalVisible); @@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) { toggleModal(); }, [installPackage, name, title, toggleModal, version]); + const handleClickUpdate = useCallback(() => { + installPackage({ name, version, title, fromUpdate: true }); + }, [installPackage, name, title, version]); + const handleClickUninstall = useCallback(() => { uninstallPackage({ name, version, title }); toggleModal(); @@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) { </EuiButton> ); + const updateButton = ( + <EuiButton iconType={'refresh'} isLoading={isInstalling} onClick={handleClickUpdate}> + <FormattedMessage + id="xpack.ingestManager.integrations.updatePackage.updatePackageButtonLabel" + defaultMessage="Update to latest version" + /> + </EuiButton> + ); + const uninstallButton = ( <EuiButton iconType={'trash'} @@ -129,7 +144,7 @@ export function InstallationButton(props: InstallationButtonProps) { return hasWriteCapabilites ? ( <Fragment> - {isInstalled || isRemoving ? uninstallButton : installButton} + {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} {isModalVisible && (isInstalled ? uninstallModal : installModal)} </Fragment> ) : null; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 3589a1a9444e1..4d4dba2a64e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,11 +8,22 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; import { useGetDatasources } from '../../../../hooks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallationButton } from './installation_button'; +import { UpdateIcon } from '../../components/icons'; + +const SettingsTitleCell = styled.td` + padding-right: ${props => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${props => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${props => props.theme.eui.spacerSizes.s}; +`; const NoteLabel = () => ( <FormattedMessage @@ -20,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: `${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> @@ -43,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, }} @@ -58,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 05729ccfc1af4..ab168ef1530bd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { : p.theme.eui.euiFontWeightRegular}; `; // don't display Data Sources tab if the package is not installed - if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources') + if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources') return null; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/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 653e2eb9a3a3b..b69dd6bcf8431 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx @@ -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 9c14a2e9dfed1..dd34e7260b27b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx @@ -30,7 +30,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({ onClose, agentConfigs = [], }) => { - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | null>(null); + const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); return ( <EuiFlyout onClose={onClose} size="l" maxWidth={640}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx index a0244c601cd96..1d2f3bd155622 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx @@ -7,16 +7,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonGroup, EuiSteps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useEnrollmentApiKey } from '../enrollment_api_keys'; import { ShellEnrollmentInstructions, ManualInstructions, } from '../../../../../components/enrollment_instructions'; -import { useCore, useGetAgents } from '../../../../../hooks'; +import { useCore, useGetAgents, useGetOneEnrollmentAPIKey } from '../../../../../hooks'; import { Loading } from '../../../components'; interface Props { - selectedAPIKeyId: string | null; + selectedAPIKeyId: string | undefined; } function useNewEnrolledAgents() { // New enrolled agents @@ -44,7 +43,7 @@ export const EnrollmentInstructions: React.FunctionComponent<Props> = ({ selecte const core = useCore(); const [installType, setInstallType] = useState<'quickInstall' | 'manual'>('quickInstall'); - const apiKey = useEnrollmentApiKey(selectedAPIKeyId); + const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); const newAgents = useNewEnrolledAgents(); if (!apiKey.data) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx index 89801bc6bee1e..67930e51418b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx @@ -16,17 +16,16 @@ import { EuiFieldText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useEnrollmentApiKeys } from '../enrollment_api_keys'; import { AgentConfig } from '../../../../../types'; -import { useInput, useCore, sendRequest } from '../../../../../hooks'; +import { useInput, useCore, sendRequest, useGetEnrollmentAPIKeys } from '../../../../../hooks'; import { enrollmentAPIKeyRouteService } from '../../../../../services'; interface Props { - onKeyChange: (keyId: string | null) => void; + onKeyChange: (keyId: string | undefined) => void; agentConfigs: AgentConfig[]; } -function useCreateApiKeyForm(configId: string | null, onSuccess: (keyId: string) => void) { +function useCreateApiKeyForm(configId: string | undefined, onSuccess: (keyId: string) => void) { const { notifications } = useCore(); const [isLoading, setIsLoading] = useState(false); const apiKeyNameInput = useInput(''); @@ -62,17 +61,16 @@ function useCreateApiKeyForm(configId: string | null, onSuccess: (keyId: string) } export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, agentConfigs }) => { - const enrollmentAPIKeysRequest = useEnrollmentApiKeys({ - currentPage: 1, - pageSize: 1000, + const enrollmentAPIKeysRequest = useGetEnrollmentAPIKeys({ + page: 1, + perPage: 1000, }); const [selectedState, setSelectedState] = useState<{ - agentConfigId: string | null; - enrollmentAPIKeyId: string | null; + agentConfigId?: string; + enrollmentAPIKeyId?: string; }>({ - agentConfigId: agentConfigs.length ? agentConfigs[0].id : null, - enrollmentAPIKeyId: null, + agentConfigId: agentConfigs.length ? agentConfigs[0].id : undefined, }); const filteredEnrollmentAPIKeys = React.useMemo(() => { if (!selectedState.agentConfigId || !enrollmentAPIKeysRequest.data) { @@ -99,10 +97,10 @@ export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, a const [showAPIKeyForm, setShowAPIKeyForm] = useState(false); const apiKeyForm = useCreateApiKeyForm(selectedState.agentConfigId, async (keyId: string) => { - const res = await enrollmentAPIKeysRequest.refresh(); + const res = await enrollmentAPIKeysRequest.sendRequest(); setSelectedState({ ...selectedState, - enrollmentAPIKeyId: res.data?.list.find(key => key.id === keyId)?.id ?? null, + enrollmentAPIKeyId: res.data?.list.find(key => key.id === keyId)?.id, }); setShowAPIKeyForm(false); }); @@ -135,7 +133,7 @@ export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, a onChange={e => setSelectedState({ agentConfigId: e.target.value, - enrollmentAPIKeyId: null, + enrollmentAPIKeyId: undefined, }) } /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx deleted file mode 100644 index 8ce20a85e14b8..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ConfirmDeleteModal: React.FunctionComponent<{ - onConfirm: () => void; - onCancel: () => void; - apiKeyId: string; -}> = ({ onConfirm, onCancel, apiKeyId }) => { - return ( - <EuiOverlayMask> - <EuiConfirmModal - title={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.title" - defaultMessage="Delete api key: {apiKeyId}" - values={{ - apiKeyId, - }} - /> - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel" - defaultMessage="Cancel" - /> - } - confirmButtonText={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel" - defaultMessage="Delete" - /> - } - buttonColor="danger" - /> - </EuiOverlayMask> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx deleted file mode 100644 index 009080a4da186..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiSelect, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useInput, sendRequest } from '../../../../../hooks'; -import { useConfigs } from './hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../../services'; - -export const CreateApiKeyForm: React.FunctionComponent<{ onChange: () => void }> = ({ - onChange, -}) => { - const { data: configs } = useConfigs(); - const { inputs, onSubmit, submitted } = useCreateApiKey(() => onChange()); - - return ( - <EuiFlexGroup style={{ maxWidth: 600 }}> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.ingestManager.apiKeysForm.nameLabel', { - defaultMessage: 'Key Name', - })} - > - <EuiFieldText autoComplete={'false'} {...inputs.nameInput.props} /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.ingestManager.apiKeysForm.configLabel', { - defaultMessage: 'Config', - })} - > - <EuiSelect - {...inputs.configIdInput.props} - options={configs.map(config => ({ - value: config.id, - text: config.name, - }))} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton disabled={submitted} onClick={() => onSubmit()}> - <FormattedMessage - id="xpack.ingestManager.apiKeysForm.saveButton" - defaultMessage="Save" - /> - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -function useCreateApiKey(onSuccess: () => void) { - const [submitted, setSubmitted] = React.useState(false); - const inputs = { - nameInput: useInput(), - configIdInput: useInput('default'), - }; - - const onSubmit = async () => { - setSubmitted(true); - await sendRequest({ - method: 'post', - path: enrollmentAPIKeyRouteService.getCreatePath(), - body: JSON.stringify({ - name: inputs.nameInput.value, - config_id: inputs.configIdInput.value, - }), - }); - setSubmitted(false); - onSuccess(); - }; - - return { - inputs, - onSubmit, - submitted, - }; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx deleted file mode 100644 index 41c6b5912cd31..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Pagination, - useGetAgentConfigs, - useGetEnrollmentAPIKeys, - useGetOneEnrollmentAPIKey, -} from '../../../../../hooks'; - -export function useEnrollmentApiKeys(pagination: Pagination) { - const request = useGetEnrollmentAPIKeys({ - page: pagination.currentPage, - perPage: pagination.pageSize, - }); - - return { - data: request.data, - isLoading: request.isLoading, - refresh: () => request.sendRequest(), - }; -} - -export function useConfigs() { - const request = useGetAgentConfigs(); - - return { - data: request.data ? request.data.items : [], - isLoading: request.isLoading, - }; -} - -export function useEnrollmentApiKey(apiKeyId: string | null) { - const request = useGetOneEnrollmentAPIKey(apiKeyId as string); - - return { - data: request.data, - isLoading: request.isLoading, - }; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx deleted file mode 100644 index 19957e7827680..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { EuiBasicTable, EuiButtonEmpty, EuiSpacer, EuiPopover, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { usePagination, sendRequest } from '../../../../../hooks'; -import { useEnrollmentApiKeys, useEnrollmentApiKey } from './hooks'; -import { ConfirmDeleteModal } from './confirm_delete_modal'; -import { CreateApiKeyForm } from './create_api_key_form'; -import { EnrollmentAPIKey } from '../../../../../types'; -import { useCapabilities } from '../../../../../hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../../services'; -export { useEnrollmentApiKeys, useEnrollmentApiKey } from './hooks'; - -export const EnrollmentApiKeysTable: React.FunctionComponent<{ - onChange: () => void; -}> = ({ onChange }) => { - const [confirmDeleteApiKeyId, setConfirmDeleteApiKeyId] = useState<string | null>(null); - const { pagination } = usePagination(); - const { data, isLoading, refresh } = useEnrollmentApiKeys(pagination); - - const columns: any[] = [ - { - field: 'name', - name: i18n.translate('xpack.ingestManager.apiKeysList.nameColumnTitle', { - defaultMessage: 'Name', - }), - width: '300px', - }, - { - field: 'config_id', - name: i18n.translate('xpack.ingestManager.apiKeysList.configColumnTitle', { - defaultMessage: 'Config', - }), - width: '100px', - }, - { - field: null, - name: i18n.translate('xpack.ingestManager.apiKeysList.apiKeyColumnTitle', { - defaultMessage: 'API Key', - }), - render: (key: EnrollmentAPIKey) => <ApiKeyField apiKeyId={key.id} />, - }, - { - field: null, - width: '50px', - render: (key: EnrollmentAPIKey) => { - return ( - <EuiButtonEmpty onClick={() => setConfirmDeleteApiKeyId(key.id)} iconType={'trash'} /> - ); - }, - }, - ]; - - return ( - <> - {confirmDeleteApiKeyId && ( - <ConfirmDeleteModal - apiKeyId={confirmDeleteApiKeyId} - onCancel={() => setConfirmDeleteApiKeyId(null)} - onConfirm={async () => { - await sendRequest({ - method: 'delete', - path: enrollmentAPIKeyRouteService.getDeletePath(confirmDeleteApiKeyId), - }); - setConfirmDeleteApiKeyId(null); - refresh(); - }} - /> - )} - <EuiBasicTable - compressed={true} - loading={isLoading} - noItemsMessage={ - <FormattedMessage - id="xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage" - defaultMessage="No api keys" - /> - } - items={data ? data.list : []} - itemId="id" - columns={columns} - /> - <EuiSpacer size={'s'} /> - <CreateApiKeyButton - onChange={() => { - refresh(); - onChange(); - }} - /> - </> - ); -}; - -export const CreateApiKeyButton: React.FunctionComponent<{ onChange: () => void }> = ({ - onChange, -}) => { - const hasWriteCapabilites = useCapabilities().write; - const [isOpen, setIsOpen] = React.useState(false); - - return ( - <EuiPopover - ownFocus - button={ - <EuiLink disabled={!hasWriteCapabilites} onClick={() => setIsOpen(true)} color="primary"> - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.createNewButton" - defaultMessage="Create a new key" - /> - </EuiLink> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - <CreateApiKeyForm - onChange={() => { - setIsOpen(false); - onChange(); - }} - /> - </EuiPopover> - ); - return <></>; -}; - -const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => { - const [visible, setVisible] = useState(false); - const { data } = useEnrollmentApiKey(apiKeyId); - - return ( - <> - {visible && data ? data.item.api_key : '••••••••••••••••••••••••••••'} - <EuiButtonEmpty size="xs" color={'text'} onClick={() => setVisible(!visible)}> - {visible ? ( - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.hideTableButton" - defaultMessage="Hide" - /> - ) : ( - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.viewTableButton" - defaultMessage="View" - /> - )} - </EuiButtonEmpty>{' '} - </> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/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 11a049047b787..692c60cdce38c 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/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 05d150fd9ae23..70d8e7d6882f8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { EuiButton, EuiButtonEmpty, + EuiBetaBadge, EuiPanel, EuiText, EuiTitle, @@ -19,10 +20,11 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; import { useLink, useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; const OverviewPanel = styled(EuiPanel).attrs(props => ({ paddingSize: 'm', @@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ } `; +const AlphaBadge = styled(EuiBetaBadge)` + vertical-align: top; + margin-left: ${props => props.theme.eui.paddingSizes.s}; +`; + export const IngestManagerOverview: React.FunctionComponent = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false); @@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => { id="xpack.ingestManager.overviewPageTitle" defaultMessage="Ingest Manager" /> + <AlphaBadge + iconType="beaker" + label={i18n.translate('xpack.ingestManager.alphaBadge.labelText', { + defaultMessage: 'Experimental', + })} + title={i18n.translate('xpack.ingestManager.alphaBadge.titleText', { + defaultMessage: 'Experimental', + })} + tooltipContent={i18n.translate('xpack.ingestManager.alphaBadge.tooltipText', { + defaultMessage: + 'This plugin might change or be removed in a future release and is not subject to the support SLA.', + })} + /> </h1> </EuiText> </EuiFlexItem> @@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { /> </h2> </EuiTitle> - <EuiButtonEmpty size="xs" flush="right"> + <EuiButtonEmpty size="xs" flush="right" href={useLink(DATA_STREAM_PATH)}> <FormattedMessage id="xpack.ingestManager.overviewPageDataStreamsPanelAction" defaultMessage="View data streams" 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 2f78ecd1b085e..1508f4dfaa628 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -32,6 +32,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, // API schemas - Data Streams GetDataStreamsResponse, // API schemas - Agents diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 42298960cc615..69f14854cdd0f 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -22,6 +22,7 @@ import { } from '../../types'; import { GetAgentConfigsResponse, + GetAgentConfigsResponseItem, GetOneAgentConfigResponse, CreateAgentConfigResponse, UpdateAgentConfigResponse, @@ -46,7 +47,7 @@ export const getAgentConfigsHandler: RequestHandler< await bluebird.map( items, - agentConfig => + (agentConfig: GetAgentConfigsResponseItem) => listAgents(soClient, { showInactive: true, perPage: 0, diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 56d6053a1451b..8f07e3ed1de02 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; -import { ensureInstalledPackage } from '../../services/epm/packages'; +import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; import { GetDatasourcesRequestSchema, GetOneDatasourceRequestSchema, @@ -85,12 +85,13 @@ export const createDatasourceHandler: RequestHandler< pkgName: request.body.package.name, callCluster, }); - + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: request.body.package.name, + pkgVersion: request.body.package.version, + }); newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: request.body.package.name, - pkgVersion: request.body.package.version, - }, + pkgInfo, request.body.inputs )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; } @@ -127,13 +128,14 @@ export const updateDatasourceHandler: RequestHandler< const pkg = newData.package || datasource.package; const inputs = newData.inputs || datasource.inputs; if (pkg && (newData.inputs || newData.package)) { - newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: pkg.name, - pkgVersion: pkg.version, - }, - inputs - )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: pkg.name, + pkgVersion: pkg.version, + }); + newData.inputs = (await datasourceService.assignPackageStream(pkgInfo, inputs)) as TypeOf< + typeof CreateDatasourceRequestSchema.body + >['inputs']; } const updatedDatasource = await datasourceService.update( diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 0b130e7b70101..90fe68e61bb1b 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_on: { type: 'keyword' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, + monitoring_enabled: { type: 'keyword' }, }, }, }, @@ -201,6 +202,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { enabled: { type: 'boolean' }, processors: { type: 'keyword' }, config: { type: 'flattened' }, + vars: { type: 'flattened' }, streams: { type: 'nested', properties: { @@ -209,7 +211,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { dataset: { type: 'keyword' }, processors: { type: 'keyword' }, config: { type: 'flattened' }, - pkg_stream: { type: 'flattened' }, + agent_stream: { type: 'flattened' }, + vars: { type: 'flattened' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts new file mode 100644 index 0000000000000..17758f6e3d7f1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { agentConfigService } from './agent_config'; +import { Output } from '../types'; + +function getSavedObjectMock(configAttributes: any) { + const mock = savedObjectsClientMock.create(); + + mock.get.mockImplementation(async (type: string, id: string) => { + return { + type, + id, + references: [], + attributes: configAttributes, + }; + }); + + return mock; +} + +jest.mock('./output', () => { + return { + outputService: { + getDefaultOutputId: () => 'test-id', + get: (): Output => { + return { + id: 'test-id', + is_default: true, + name: 'default', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }; + }, + }, + }; +}); + +describe('agent config', () => { + describe('getFullConfig', () => { + it('should return a config without monitoring if not monitoring is not enabled', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for logs', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['logs'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: true, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for metrics', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 75bbfc21293c2..7ab6ef1920c18 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -301,28 +301,49 @@ class AgentConfigService { if (!config) { return null; } + const defaultOutput = await outputService.get( + soClient, + await outputService.getDefaultOutputId(soClient) + ); const agentConfig: FullAgentConfig = { id: config.id, outputs: { // TEMPORARY as we only support a default output - ...[ - await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)), - ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { - outputs[name] = { - type, - hosts, - ca_sha256, - api_key, - ...outputConfig, - }; - return outputs; - }, {} as FullAgentConfig['outputs']), + ...[defaultOutput].reduce( + (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { + outputs[name] = { + type, + hosts, + ca_sha256, + api_key, + ...outputConfig, + }; + return outputs; + }, + {} as FullAgentConfig['outputs'] + ), }, datasources: (config.datasources as Datasource[]) .filter(datasource => datasource.enabled) .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, + ...(config.monitoring_enabled && config.monitoring_enabled.length > 0 + ? { + settings: { + monitoring: { + use_output: defaultOutput.name, + enabled: true, + logs: config.monitoring_enabled.indexOf('logs') >= 0, + metrics: config.monitoring_enabled.indexOf('metrics') >= 0, + }, + }, + } + : { + settings: { + monitoring: { enabled: false, logs: false, metrics: false }, + }, + }), }; return agentConfig; diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts index 09c59998388d1..3682ae6d1167b 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts @@ -5,39 +5,38 @@ */ import { datasourceService } from './datasource'; +import { PackageInfo } from '../types'; -async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { - if (dataset === 'dataset1') { - return [ - { - buffer: Buffer.from(` +const TEMPLATE = ` type: log metricset: ["dataset1"] paths: {{#each paths}} - {{this}} {{/each}} -`), - }, - ]; - } - return []; -} - -jest.mock('./epm/packages/assets', () => { - return { - getAssetsDataForPackageKey: mockedGetAssetsData, - }; -}); +`; describe('Datasource service', () => { describe('assignPackageStream', () => { - it('should work with cofig variables from the stream', async () => { + it('should work with config variables from the stream', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', @@ -47,7 +46,7 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -67,12 +66,12 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, }, - pkg_stream: { + agent_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', @@ -85,15 +84,28 @@ describe('Datasource service', () => { it('should work with config variables at the input level', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -113,7 +125,7 @@ describe('Datasource service', () => { { type: 'log', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -123,7 +135,7 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - pkg_stream: { + agent_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index f27252aaa9a84..0a5ba43e40fba 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 21de625532f03..db2e4fe474640 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -6,29 +6,61 @@ import { createStream } from './agent'; -test('Test creating a stream from template', () => { - const streamTemplate = ` -input: log -paths: -{{#each paths}} - - {{this}} -{{/each}} -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `; - const vars = { - paths: ['/usr/local/var/log/nginx/access.log'], - }; +describe('createStream', () => { + it('should work', () => { + const streamTemplate = ` + input: log + paths: + {{#each paths}} + - {{this}} + {{/each}} + exclude_files: [".gz$"] + processors: + - add_locale: ~ + `; + const vars = { + paths: { value: ['/usr/local/var/log/nginx/access.log'] }, + }; - const output = createStream(vars, streamTemplate); + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + paths: ['/usr/local/var/log/nginx/access.log'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }); + }); - expect(output).toBe(` -input: log -paths: - - /usr/local/var/log/nginx/access.log -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `); + it('should support yaml values', () => { + const streamTemplate = ` + input: redis/metrics + metricsets: ["key"] + test: null + {{#if key.patterns}} + key.patterns: {{key.patterns}} + {{/if}} + `; + const vars = { + 'key.patterns': { + type: 'yaml', + value: ` + - limit: 20 + pattern: '*' + `, + }, + }; + + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'redis/metrics', + metricsets: ['key'], + test: null, + 'key.patterns': [ + { + limit: 20, + pattern: '*', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 5d9a6d409aa1a..8254c0d8aaa37 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,12 +5,71 @@ */ import Handlebars from 'handlebars'; +import { safeLoad } from 'js-yaml'; +import { DatasourceConfigRecord } from '../../../../common'; -interface StreamVars { - [k: string]: string | string[]; +function isValidKey(key: string) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; } -export function createStream(vars: StreamVars, streamTemplate: string) { - const template = Handlebars.compile(streamTemplate); - return template(vars); +function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) { + if (Object.keys(yamlVariables).length === 0 || !yaml) { + return yaml; + } + + Object.entries(yaml).forEach(([key, value]: [string, any]) => { + if (typeof value === 'object') { + yaml[key] = replaceVariablesInYaml(yamlVariables, value); + } + if (typeof value === 'string' && value in yamlVariables) { + yaml[key] = yamlVariables[value]; + } + }); + + return yaml; +} + +function buildTemplateVariables(variables: DatasourceConfigRecord) { + const yamlValues: { [k: string]: any } = {}; + const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { + // support variables with . like key.patterns + const keyParts = key.split('.'); + const lastKeyPart = keyParts.pop(); + + if (!lastKeyPart || !isValidKey(lastKeyPart)) { + throw new Error('Invalid key'); + } + + let varPart = acc; + for (const keyPart of keyParts) { + if (!isValidKey(keyPart)) { + throw new Error('Invalid key'); + } + if (!varPart[keyPart]) { + varPart[keyPart] = {}; + } + varPart = varPart[keyPart]; + } + + if (recordEntry.type && recordEntry.type === 'yaml') { + const yamlKeyPlaceholder = `##${key}##`; + varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; + yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else { + varPart[lastKeyPart] = recordEntry.value; + } + return acc; + }, {} as { [k: string]: any }); + + return { vars, yamlValues }; +} + +export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { + const { vars, yamlValues } = buildTemplateVariables(variables); + + const template = Handlebars.compile(streamTemplate, { noEscape: true }); + const stream = template(vars); + const yamlFromStream = safeLoad(stream, {}); + + return replaceVariablesInYaml(yamlValues, yamlFromStream); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 5cf1f241a709f..440060aff9616 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -2,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 4df626259ece7..6ef6f863753b5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -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 1a73c9581a2de..3679c577ee571 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 46b6923962462..9736f6d1cbd3c 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 e3aef6077dbc3..42989bb1e3ac9 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 9c9843e0454ab..edf7624d3f0d5 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 d76584225877c..da8d79a04b97c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -67,9 +67,10 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise<PackageInfo> { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [item, savedObject, assets] = await Promise.all([ + const [item, savedObject, latestPackage, assets] = await Promise.all([ Registry.fetchInfo(pkgName, pkgVersion), getInstallationObject({ savedObjectsClient, pkgName }), + Registry.fetchFindLatestPackage(pkgName), Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 @@ -79,6 +80,7 @@ export async function getPackageInfo(options: { // add properties that aren't (or aren't yet) on Registry response const updated = { ...item, + latestVersion: latestPackage.version, title: item.title || nameAsTitle(item.name), assets: Registry.groupPathsByService(assets || []), }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f..c67cccd044bf5 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 06f3decdbbe6f..8f51c4d78305c 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 498796438c6c8..befb4722b6504 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/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 206ad76703cf5..390e240841611 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -145,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/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts index 040b2eb16289a..59cadf3bd7f74 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts @@ -11,6 +11,9 @@ const AgentConfigBaseSchema = { name: schema.string(), namespace: schema.maybe(schema.string()), description: schema.maybe(schema.string()), + monitoring_enabled: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')])) + ), }; export const NewAgentConfigSchema = schema.object({ diff --git a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts index c0cfee8f231c9..e71016560f60c 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts @@ -6,6 +6,14 @@ import { schema } from '@kbn/config-schema'; export { Datasource, NewDatasource } from '../../../common'; +const ConfigRecordSchema = schema.recordOf( + schema.string(), + schema.object({ + type: schema.maybe(schema.string()), + value: schema.maybe(schema.any()), + }) +); + const DatasourceBaseSchema = { name: schema.string(), description: schema.maybe(schema.string()), @@ -25,6 +33,7 @@ const DatasourceBaseSchema = { type: schema.string(), enabled: schema.boolean(), processors: schema.maybe(schema.arrayOf(schema.string())), + vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( schema.string(), @@ -40,6 +49,7 @@ const DatasourceBaseSchema = { enabled: schema.boolean(), dataset: schema.string(), processors: schema.maybe(schema.arrayOf(schema.string())), + vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( schema.string(), diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md index 030bba760499d..a469511bdbbd2 100644 --- a/x-pack/plugins/ingest_pipelines/README.md +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -1,9 +1,24 @@ -# ingest_pipelines +# Ingest Node Pipelines UI -> Ingest node pipelines UI +## Summary +The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest nodes](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). Please refer to the Elasticsearch documentation for more details. + +This plugin allows Kibana to create, edit, clone and delete ingest node pipelines. It also provides support to simulate a pipeline. + +It requires a Basic license and the following cluster privileges: `manage_pipeline` and `cluster:monitor/nodes/info`. --- ## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. +A new app called Ingest Node Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions on setting up your development environment. + +### Test coverage + +The app has the following test coverage: + +- Complete API integration tests +- Smoke-level functional test +- Client-integration tests diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 2ec72267701d7..ba7675b507596 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -26,6 +26,8 @@ export const AppWithoutRouter = () => ( <Route exact path={`${BASE_PATH}/create/:sourceName`} component={PipelinesClone} /> <Route exact path={`${BASE_PATH}/create`} component={PipelinesCreate} /> <Route exact path={`${BASE_PATH}/edit/:name`} component={PipelinesEdit} /> + {/* Catch all */} + <Route component={PipelinesList} /> </Switch> ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts index ec92d899fd1cd..21a2ee30a84e1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -5,5 +5,3 @@ */ export { PipelineForm } from './pipeline_form'; - -export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 703ffd1589a56..ee9b5b576118a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,7 +11,7 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { PipelineRequestFlyout } from '../'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; @@ -106,8 +106,6 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({ isInvalid={form.isSubmitted && !form.isValid} error={form.getErrors()} > - <EuiSpacer size="l" /> - {/* Request error */} {saveError && <PipelineFormError errorMessage={saveError.message} />} @@ -124,9 +122,9 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({ {/* Form submission */} <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> + <EuiFlexItem> <EuiFlexGroup> - <EuiFlexItem> + <EuiFlexItem grow={false}> <EuiButton fill color="secondary" @@ -139,7 +137,7 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({ {saveButtonLabel} </EuiButton> </EuiFlexItem> - <EuiFlexItem> + <EuiFlexItem grow={false}> <EuiButtonEmpty color="primary" onClick={onCancel}> <FormattedMessage id="xpack.ingestPipelines.form.cancelButtonLabel" diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts new file mode 100644 index 0000000000000..9476b65557c54 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx similarity index 95% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx index a5184a20630d5..58e86695808b1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { Pipeline } from '../../../common/types'; +import { Pipeline } from '../../../../../common/types'; interface Props { pipeline: Pipeline; @@ -40,7 +40,7 @@ export const PipelineRequestFlyout: React.FunctionComponent<Props> = ({ uuid.current++; return ( - <EuiFlyout maxWidth={480} onClose={closeFlyout}> + <EuiFlyout maxWidth={550} onClose={closeFlyout}> <EuiFlyoutHeader> <EuiTitle> <h2> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx similarity index 89% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx index 8f8d89772c964..6dcedca6085af 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -6,8 +6,8 @@ import React, { useState, useEffect } from 'react'; -import { Pipeline } from '../../../common/types'; -import { useFormContext } from '../../shared_imports'; +import { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx index c0f4b4a7a0aed..16f39b2912c1d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -128,7 +128,7 @@ export const PipelineTestFlyout: React.FunctionComponent<PipelineTestFlyoutProps } return ( - <EuiFlyout maxWidth={480} onClose={closeFlyout}> + <EuiFlyout maxWidth={550} onClose={closeFlyout}> <EuiFlyoutHeader> <EuiTitle> <h2> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts deleted file mode 100644 index 21a03a3076248..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { i18n } from '@kbn/i18n'; - -import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; -import { parseJson, stringifyJson } from '../../../../lib'; - -const { emptyField, isJsonField } = fieldValidators; - -export const documentsSchema: FormSchema = { - documents: { - label: i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', - { - defaultMessage: 'Documents', - } - ), - serializer: parseJson, - deserializer: stringifyJson, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', - { - defaultMessage: 'Documents are required.', - } - ) - ), - }, - { - validator: isJsonField( - i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError', - { - defaultMessage: 'The documents JSON is not valid.', - } - ) - ), - }, - { - validator: ({ value }: ValidationFuncArg<any, any>) => { - const parsedJSON = JSON.parse(value); - - if (!parsedJSON.length) { - return { - message: i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', - { - defaultMessage: 'At least one document is required.', - } - ), - }; - } - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx new file mode 100644 index 0000000000000..de9910344bd4b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; + +import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; +import { parseJson, stringifyJson } from '../../../../lib'; + +const { emptyField, isJsonField } = fieldValidators; + +export const documentsSchema: FormSchema = { + documents: { + label: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', + { + defaultMessage: 'Documents', + } + ), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', + { + defaultMessage: 'Documents are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError', + { + defaultMessage: 'The documents JSON is not valid.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg<any, any>) => { + const parsedJSON = JSON.parse(value); + + if (!parsedJSON.length) { + return { + message: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', + { + defaultMessage: 'At least one document is required.', + } + ), + }; + } + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx index 79d2031ffa91b..97bf03dbdc068 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule } from '@elastic/eui'; +import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule, EuiLink } from '@elastic/eui'; import { getUseField, @@ -17,6 +17,7 @@ import { Form, useForm, FormConfig, + useKibana, } from '../../../../../shared_imports'; import { documentsSchema } from './schema'; @@ -35,6 +36,8 @@ export const DocumentsTab: React.FunctionComponent<Props> = ({ handleExecute, isExecuting, }) => { + const { services } = useKibana(); + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; @@ -69,7 +72,19 @@ export const DocumentsTab: React.FunctionComponent<Props> = ({ <p> <FormattedMessage id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText" - defaultMessage="Provide an array of documents to be ingested by the pipeline." + defaultMessage="Provide an array of documents to be ingested by the pipeline. {learnMoreLink}" + values={{ + learnMoreLink: ( + <EuiLink href={services.documentation.getSimulatePipelineApiUrl()} target="_blank"> + {i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', + { + defaultMessage: 'Learn more.', + } + )} + </EuiLink> + ), + }} /> </p> </EuiText> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index d449e1af5f8c8..2e2689f41527a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -3,8 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; import { parseJson, stringifyJson } from '../../lib'; @@ -47,6 +50,26 @@ export const pipelineFormSchema: FormSchema = { label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { defaultMessage: 'Processors', }), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.processorsFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + set: { + field: 'foo', + value: 'bar', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), serializer: parseJson, deserializer: stringifyJson, validations: [ @@ -70,6 +93,26 @@ export const pipelineFormSchema: FormSchema = { label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { defaultMessage: 'On-failure processors (optional)', }), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + set: { + field: '_index', + value: 'failed-{{ _index }}', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), serializer: value => { const result = parseJson(value); // If an empty array was passed, strip out this value entirely. diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index d601e9f9c245b..34a362d596d92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, + EuiSpacer, } from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; @@ -49,7 +50,7 @@ export const PipelinesCreate: React.FunctionComponent<RouteComponentProps & Prop return; } - history.push(BASE_PATH); + history.push(BASE_PATH + `?pipeline=${pipeline.name}`); }; const onCancel = () => { @@ -93,6 +94,8 @@ export const PipelinesCreate: React.FunctionComponent<RouteComponentProps & Prop </EuiFlexGroup> </EuiTitle> + <EuiSpacer size="l" /> + <PipelineForm defaultValue={sourcePipeline} onSave={onSave} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index 02937b5618143..99cd8d7eef97b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -54,7 +54,7 @@ export const PipelinesEdit: React.FunctionComponent<RouteComponentProps<MatchPar return; } - history.push(BASE_PATH); + history.push(BASE_PATH + `?pipeline=${updatedPipeline.name}`); }; const onCancel = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx deleted file mode 100644 index 07fdb1c15e0bf..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiTitle, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiIcon, - EuiPopover, - EuiContextMenu, - EuiButton, -} from '@elastic/eui'; - -import { Pipeline } from '../../../../common/types'; - -import { PipelineDetailsJsonBlock } from './details_json_block'; - -export interface Props { - pipeline: Pipeline; - onEditClick: (pipelineName: string) => void; - onCloneClick: (pipelineName: string) => void; - onDeleteClick: (pipelineName: string[]) => void; - onClose: () => void; -} - -export const PipelineDetails: FunctionComponent<Props> = ({ - pipeline, - onClose, - onEditClick, - onCloneClick, - onDeleteClick, -}) => { - const [showPopover, setShowPopover] = useState(false); - const actionMenuItems = [ - /** - * Edit pipeline - */ - { - name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { - defaultMessage: 'Edit', - }), - icon: <EuiIcon type="pencil" />, - onClick: () => onEditClick(pipeline.name), - }, - /** - * Clone pipeline - */ - { - name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { - defaultMessage: 'Clone', - }), - icon: <EuiIcon type="copy" />, - onClick: () => onCloneClick(pipeline.name), - }, - /** - * Delete pipeline - */ - { - name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { - defaultMessage: 'Delete', - }), - icon: <EuiIcon type="trash" />, - onClick: () => onDeleteClick([pipeline.name]), - }, - ]; - - const managePipelineButton = ( - <EuiButton - data-test-subj="managePipelineButton" - aria-label={i18n.translate( - 'xpack.ingestPipelines.list.pipelineDetails.managePipelineActionsAriaLabel', - { - defaultMessage: 'Manage pipeline', - } - )} - onClick={() => setShowPopover(previousBool => !previousBool)} - iconType="arrowUp" - iconSide="right" - fill - > - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { - defaultMessage: 'Manage', - })} - </EuiButton> - ); - - return ( - <EuiFlyout - onClose={onClose} - aria-labelledby="pipelineDetailsFlyoutTitle" - size="m" - maxWidth={550} - > - <EuiFlyoutHeader> - <EuiTitle id="pipelineDetailsFlyoutTitle"> - <h2>{pipeline.name}</h2> - </EuiTitle> - </EuiFlyoutHeader> - - <EuiFlyoutBody> - <EuiDescriptionList> - {/* Pipeline description */} - <EuiDescriptionListTitle> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { - defaultMessage: 'Description', - })} - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - {pipeline.description ?? ''} - </EuiDescriptionListDescription> - - {/* Pipeline version */} - {pipeline.version && ( - <> - <EuiDescriptionListTitle> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { - defaultMessage: 'Version', - })} - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - {String(pipeline.version)} - </EuiDescriptionListDescription> - </> - )} - - {/* Processors JSON */} - <EuiDescriptionListTitle> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.processorsTitle', { - defaultMessage: 'Processors JSON', - })} - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - <PipelineDetailsJsonBlock json={pipeline.processors} /> - </EuiDescriptionListDescription> - - {/* On Failure Processor JSON */} - {pipeline.on_failure?.length && ( - <> - <EuiDescriptionListTitle> - {i18n.translate( - 'xpack.ingestPipelines.list.pipelineDetails.failureProcessorsTitle', - { - defaultMessage: 'On failure processors JSON', - } - )} - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - <PipelineDetailsJsonBlock json={pipeline.on_failure} /> - </EuiDescriptionListDescription> - </> - )} - </EuiDescriptionList> - </EuiFlyoutBody> - - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel', { - defaultMessage: 'Close', - })} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiPopover - isOpen={showPopover} - closePopover={() => setShowPopover(false)} - button={managePipelineButton} - panelPaddingSize="none" - withTitle - repositionOnScroll - > - <EuiContextMenu - initialPanelId={0} - data-test-subj="autoFollowPatternActionContextMenu" - panels={[ - { - id: 0, - title: i18n.translate( - 'xpack.ingestPipelines.list.pipelineDetails.managePipelinePanelTitle', - { - defaultMessage: 'Pipeline options', - } - ), - items: actionMenuItems, - }, - ]} - /> - </EuiPopover> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexGroup> - </EuiFlyoutFooter> - </EuiFlyout> - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx new file mode 100644 index 0000000000000..98243a5149c0d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiIcon, + EuiPopover, + EuiContextMenu, + EuiButton, +} from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; + +import { PipelineDetailsJsonBlock } from './details_json_block'; + +export interface Props { + pipeline: Pipeline; + onEditClick: (pipelineName: string) => void; + onCloneClick: (pipelineName: string) => void; + onDeleteClick: (pipelineName: string[]) => void; + onClose: () => void; +} + +export const PipelineDetailsFlyout: FunctionComponent<Props> = ({ + pipeline, + onClose, + onEditClick, + onCloneClick, + onDeleteClick, +}) => { + const [showPopover, setShowPopover] = useState(false); + const actionMenuItems = [ + /** + * Edit pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { + defaultMessage: 'Edit', + }), + icon: <EuiIcon type="pencil" />, + onClick: () => onEditClick(pipeline.name), + }, + /** + * Clone pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: <EuiIcon type="copy" />, + onClick: () => onCloneClick(pipeline.name), + }, + /** + * Delete pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { + defaultMessage: 'Delete', + }), + icon: <EuiIcon type="trash" />, + onClick: () => onDeleteClick([pipeline.name]), + }, + ]; + + const managePipelineButton = ( + <EuiButton + data-test-subj="managePipelineButton" + aria-label={i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.managePipelineActionsAriaLabel', + { + defaultMessage: 'Manage pipeline', + } + )} + onClick={() => setShowPopover(previousBool => !previousBool)} + iconType="arrowUp" + iconSide="right" + fill + > + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { + defaultMessage: 'Manage', + })} + </EuiButton> + ); + + return ( + <EuiFlyout + onClose={onClose} + aria-labelledby="pipelineDetailsFlyoutTitle" + size="m" + maxWidth={550} + > + <EuiFlyoutHeader> + <EuiTitle id="pipelineDetailsFlyoutTitle"> + <h2>{pipeline.name}</h2> + </EuiTitle> + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <EuiDescriptionList> + {/* Pipeline description */} + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {pipeline.description ?? ''} + </EuiDescriptionListDescription> + + {/* Pipeline version */} + {pipeline.version && ( + <> + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { + defaultMessage: 'Version', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {String(pipeline.version)} + </EuiDescriptionListDescription> + </> + )} + + {/* Processors JSON */} + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.processorsTitle', { + defaultMessage: 'Processors JSON', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <PipelineDetailsJsonBlock json={pipeline.processors} /> + </EuiDescriptionListDescription> + + {/* On Failure Processor JSON */} + {pipeline.on_failure?.length && ( + <> + <EuiDescriptionListTitle> + {i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.failureProcessorsTitle', + { + defaultMessage: 'On failure processors JSON', + } + )} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <PipelineDetailsJsonBlock json={pipeline.on_failure} /> + </EuiDescriptionListDescription> + </> + )} + </EuiDescriptionList> + </EuiFlyoutBody> + + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel', { + defaultMessage: 'Close', + })} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiPopover + isOpen={showPopover} + closePopover={() => setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + > + <EuiContextMenu + initialPanelId={0} + data-test-subj="autoFollowPatternActionContextMenu" + panels={[ + { + id: 0, + title: i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.managePipelinePanelTitle', + { + defaultMessage: 'Pipeline options', + } + ), + items: actionMenuItems, + }, + ]} + /> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 8af76460b75ae..c90ac2714a95a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -8,6 +8,8 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { Location } from 'history'; +import { parse } from 'query-string'; import { EuiPageBody, @@ -28,33 +30,58 @@ import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; -import { PipelineDetails } from './details'; +import { PipelineDetailsFlyout } from './details_flyout'; +import { PipelineNotFoundFlyout } from './not_found_flyout'; import { PipelineDeleteModal } from './delete_modal'; -export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ history }) => { +const getPipelineNameFromLocation = (location: Location) => { + const { pipeline } = parse(location.search.substring(1)); + return pipeline; +}; + +export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ + history, + location, +}) => { const { services } = useKibana(); + const pipelineNameFromLocation = getPipelineNameFromLocation(location); const [selectedPipeline, setSelectedPipeline] = useState<Pipeline | undefined>(undefined); + const [showFlyout, setShowFlyout] = useState<boolean>(false); + const [pipelinesToDelete, setPipelinesToDelete] = useState<string[]>([]); + const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + // Track component loaded useEffect(() => { services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); services.breadcrumbs.setBreadcrumbs('home'); }, [services.metric, services.breadcrumbs]); - const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + useEffect(() => { + if (pipelineNameFromLocation && data?.length) { + const pipeline = data.find(p => p.name === pipelineNameFromLocation); + setSelectedPipeline(pipeline); + setShowFlyout(true); + } + }, [pipelineNameFromLocation, data]); - let content: React.ReactNode; + const goToEditPipeline = (name: string) => { + history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + }; - const editPipeline = (name: string) => { - history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); + const goToClonePipeline = (name: string) => { + history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); }; - const clonePipeline = (name: string) => { - history.push(encodeURI(`${BASE_PATH}/create/${encodeURIComponent(name)}`)); + const goHome = () => { + setShowFlyout(false); + history.push(BASE_PATH); }; + let content: React.ReactNode; + if (isLoading) { content = ( <SectionLoading> @@ -68,10 +95,9 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi content = ( <PipelineTable onReloadClick={sendRequest} - onEditPipelineClick={editPipeline} + onEditPipelineClick={goToEditPipeline} onDeletePipelineClick={setPipelinesToDelete} - onClonePipelineClick={clonePipeline} - onViewPipelineClick={setSelectedPipeline} + onClonePipelineClick={goToClonePipeline} pipelines={data} /> ); @@ -79,6 +105,37 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi content = <EmptyList />; } + const renderFlyout = (): React.ReactNode => { + if (!showFlyout) { + return; + } + if (selectedPipeline) { + return ( + <PipelineDetailsFlyout + pipeline={selectedPipeline} + onClose={() => { + setSelectedPipeline(undefined); + goHome(); + }} + onEditClick={goToEditPipeline} + onCloneClick={goToClonePipeline} + onDeleteClick={setPipelinesToDelete} + /> + ); + } else { + // Somehow we triggered show pipeline details, but do not have a pipeline. + // We assume not found. + return ( + <PipelineNotFoundFlyout + onClose={() => { + goHome(); + }} + pipelineName={pipelineNameFromLocation} + /> + ); + } + }; + return ( <> <EuiPageBody> @@ -131,15 +188,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ hi )} </EuiPageContent> </EuiPageBody> - {selectedPipeline && ( - <PipelineDetails - pipeline={selectedPipeline} - onClose={() => setSelectedPipeline(undefined)} - onEditClick={editPipeline} - onCloneClick={clonePipeline} - onDeleteClick={setPipelinesToDelete} - /> - )} + {renderFlyout()} {pipelinesToDelete?.length > 0 ? ( <PipelineDeleteModal callback={deleteResponse => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx new file mode 100644 index 0000000000000..b967e54187ced --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlyout, EuiFlyoutBody, EuiCallOut } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +interface Props { + onClose: () => void; + pipelineName: string | string[] | null | undefined; +} + +export const PipelineNotFoundFlyout: FunctionComponent<Props> = ({ onClose, pipelineName }) => { + return ( + <EuiFlyout onClose={onClose} size="m" maxWidth={550}> + <EuiFlyoutHeader> + {pipelineName && ( + <EuiTitle id="notFoundFlyoutTitle"> + <h2>{pipelineName}</h2> + </EuiTitle> + )} + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.list.notFoundFlyoutMessage" + defaultMessage="Pipeline not found" + /> + } + color="danger" + iconType="alert" + /> + </EuiFlyoutBody> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 05488f46c148e..c93285289ff39 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui'; +import { EuiInMemoryTable, EuiLink, EuiButton, EuiInMemoryTableProps } from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; @@ -17,7 +17,6 @@ export interface Props { onEditPipelineClick: (pipelineName: string) => void; onClonePipelineClick: (pipelineName: string) => void; onDeletePipelineClick: (pipelineName: string[]) => void; - onViewPipelineClick: (pipeline: Pipeline) => void; } export const PipelineTable: FunctionComponent<Props> = ({ @@ -26,126 +25,124 @@ export const PipelineTable: FunctionComponent<Props> = ({ onEditPipelineClick, onClonePipelineClick, onDeletePipelineClick, - onViewPipelineClick, }) => { const [selection, setSelection] = useState<Pipeline[]>([]); - return ( - <EuiInMemoryTable - itemId="name" - isSelectable - sorting={{ sort: { field: 'name', direction: 'asc' } }} - selection={{ - onSelectionChange: setSelection, - }} - search={{ - toolsLeft: - selection.length > 0 ? ( - <EuiButton - data-test-subj="deletePipelinesButton" - onClick={() => onDeletePipelineClick(selection.map(pipeline => pipeline.name))} - color="danger" - > - <FormattedMessage - id="xpack.ingestPipelines.list.table.deletePipelinesButtonLabel" - defaultMessage="Delete {count, plural, one {pipeline} other {pipelines} }" - values={{ count: selection.length }} - /> - </EuiButton> - ) : ( - undefined - ), - toolsRight: [ + const tableProps: EuiInMemoryTableProps<Pipeline> = { + itemId: 'name', + isSelectable: true, + sorting: { sort: { field: 'name', direction: 'asc' } }, + selection: { + onSelectionChange: setSelection, + }, + search: { + toolsLeft: + selection.length > 0 ? ( <EuiButton - key="reloadButton" - iconType="refresh" - color="secondary" - data-test-subj="reloadButton" - onClick={onReloadClick} + data-test-subj="deletePipelinesButton" + onClick={() => onDeletePipelineClick(selection.map(pipeline => pipeline.name))} + color="danger" > - {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { - defaultMessage: 'Reload', - })} - </EuiButton>, - <EuiButton - href={`#${BASE_PATH}/create`} - fill - iconType="plusInCircle" - data-test-subj="createPipelineButton" - key="createPipelineButton" - > - {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create a pipeline', - })} - </EuiButton>, + <FormattedMessage + id="xpack.ingestPipelines.list.table.deletePipelinesButtonLabel" + defaultMessage="Delete {count, plural, one {pipeline} other {pipelines} }" + values={{ count: selection.length }} + /> + </EuiButton> + ) : ( + undefined + ), + toolsRight: [ + <EuiButton + key="reloadButton" + iconType="refresh" + color="secondary" + data-test-subj="reloadButton" + onClick={onReloadClick} + > + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { + defaultMessage: 'Reload', + })} + </EuiButton>, + <EuiButton + href={`#${BASE_PATH}/create`} + fill + iconType="plusInCircle" + data-test-subj="createPipelineButton" + key="createPipelineButton" + > + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + </EuiButton>, + ], + box: { + incremental: true, + }, + }, + pagination: { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }, + columns: [ + { + field: 'name', + name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + sortable: true, + render: (name: string) => <EuiLink href={`#${BASE_PATH}?pipeline=${name}`}>{name}</EuiLink>, + }, + { + name: ( + <FormattedMessage + id="xpack.ingestPipelines.list.table.actionColumnTitle" + defaultMessage="Actions" + /> + ), + actions: [ + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.editActionDescription', { + defaultMessage: 'Edit this pipeline', + }), + type: 'icon', + icon: 'pencil', + onClick: ({ name }) => onEditPipelineClick(name), + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.cloneActionDescription', { + defaultMessage: 'Clone this pipeline', + }), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.deleteActionDescription', + { defaultMessage: 'Delete this pipeline' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: ({ name }) => onDeletePipelineClick([name]), + }, ], - box: { - incremental: true, - }, - }} - pagination={{ - initialPageSize: 10, - pageSizeOptions: [10, 20, 50], - }} - columns={[ - { - field: 'name', - name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { - defaultMessage: 'Name', - }), - sortable: true, - render: (name: string, pipeline) => ( - <EuiLink onClick={() => onViewPipelineClick(pipeline)}>{name}</EuiLink> - ), - }, - { - name: i18n.translate('xpack.ingestPipelines.list.table.actionColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - isPrimary: true, - name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.editActionDescription', - { defaultMessage: 'Edit this pipeline' } - ), - type: 'icon', - icon: 'pencil', - onClick: ({ name }) => onEditPipelineClick(name), - }, - { - name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { - defaultMessage: 'Clone', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.cloneActionDescription', - { defaultMessage: 'Clone this pipeline' } - ), - type: 'icon', - icon: 'copy', - onClick: ({ name }) => onClonePipelineClick(name), - }, - { - isPrimary: true, - name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.deleteActionDescription', - { defaultMessage: 'Delete this pipeline' } - ), - type: 'icon', - icon: 'trash', - color: 'danger', - onClick: ({ name }) => onDeletePipelineClick([name]), - }, - ], - }, - ]} - items={pipelines ?? []} - /> - ); + }, + ], + items: pipelines ?? [], + }; + + return <EuiInMemoryTable {...tableProps} />; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index d443ed83eb388..05fdc4b1dfb84 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -30,6 +30,10 @@ export class DocumentationService { public getPutPipelineApiUrl() { return `${this.esDocBasePath}/put-pipeline-api.html`; } + + public getSimulatePipelineApiUrl() { + return `${this.esDocBasePath}/simulate-pipeline-api.html`; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/index.scss b/x-pack/plugins/ingest_pipelines/public/index.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 1acfe8f36b457..7247973703804 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './index.scss'; - import { IngestPipelinesPlugin } from './plugin'; export function plugin() { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 90ead800e5ddf..ec92262014272 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -30,6 +30,12 @@ export const registerGetRoutes = ({ return res.ok({ body: deserializePipelines(pipelines) }); } catch (error) { if (isEsError(error)) { + // ES returns 404 when there are no pipelines + // Instead, we return an empty array and 200 status back to the client + if (error.status === 404) { + return res.ok({ body: [] }); + } + return res.customError({ statusCode: error.statusCode, body: error, diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index 78c29d061fe5a..ca5fc78d118fd 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -33,9 +33,8 @@ export const registerSimulateRoute = ({ }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; - const reqBody = req.body; - const { pipeline, documents, verbose } = reqBody; + const { pipeline, documents, verbose } = req.body; try { const response = await callAsCurrentUser('ingest.simulate', { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 27a3c9fb97ef8..a6fdee47f0ecf 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { Pipeline } from '../../../common/types'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; @@ -36,9 +35,7 @@ export const registerUpdateRoute = ({ license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; const { name } = req.params; - const pipeline = req.body as Pipeline; - - const { description, processors, version, on_failure } = pipeline; + const { description, processors, version, on_failure } = req.body; try { // Verify pipeline exists; ES will throw 404 if it doesn't diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 359c06a6a9ebc..48729448b2ea5 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -41,6 +41,10 @@ export const datatableVisualization: Visualization< }, ], + getVisualizationTypeId() { + return 'lnsDatatable'; + }, + getLayerIds(state) { return state.layers.map(l => l.layerId); }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 3c61d270b1bcf..c8d8064e60e38 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -62,7 +62,25 @@ describe('chart_switch', () => { id: 'subvisC2', label: 'C2', }, + { + icon: 'empty', + id: 'subvisC3', + label: 'C3', + }, ], + getSuggestions: jest.fn(options => { + if (options.subVisualizationId === 'subvisC2') { + return []; + } + return [ + { + score: 1, + title: '', + state: `suggestion`, + previewIcon: 'empty', + }, + ]; + }), }, }; } @@ -313,10 +331,11 @@ describe('chart_switch', () => { expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined(); }); - it('should not indicate data loss if visualization is not changed', () => { + it('should not show a warning when the subvisualization is the same', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); + visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); const switchVisualizationType = jest.fn(() => 'therebedragons'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -333,10 +352,10 @@ describe('chart_switch', () => { /> ); - expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined(); + expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); }); - it('should remove all layers if there is no suggestion', () => { + it('should get suggestions when switching subvisualization', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); visualizations.visB.getSuggestions.mockReturnValueOnce([]); @@ -377,7 +396,7 @@ describe('chart_switch', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(() => 'therebedragons'); + const switchVisualizationType = jest.fn(() => 'switched'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -393,12 +412,12 @@ describe('chart_switch', () => { /> ); - switchTo('subvisC2', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins'); + switchTo('subvisC3', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion'); expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: 'SWITCH_VISUALIZATION', - initialState: 'therebedragons', + initialState: 'switched', }) ); expect(frame.removeLayers).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 1461449f3c1c8..d73f83e75c0e4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) { const switchVisType = props.visualizationMap[visualizationId].switchVisualizationType || ((_type: string, initialState: unknown) => initialState); - if (props.visualizationId === visualizationId) { + const layers = Object.entries(props.framePublicAPI.datasourceLayers); + const containsData = layers.some( + ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + ); + // Always show the active visualization as a valid selection + if ( + props.visualizationId === visualizationId && + props.visualizationState && + newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId + ) { return { visualizationId, subVisualizationId, @@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) { }; } - const layers = Object.entries(props.framePublicAPI.datasourceLayers); - const containsData = layers.some( - ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + const topSuggestion = getTopSuggestion( + props, + visualizationId, + newVisualization, + subVisualizationId ); - const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization); - let dataLoss: VisualizationSelection['dataLoss']; if (!containsData) { @@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization<unknown, unknown> + newVisualization: Visualization<unknown, unknown>, + subVisualizationId?: string ): Suggestion | undefined { const suggestions = getSuggestions({ datasourceMap: props.datasourceMap, @@ -258,6 +268,7 @@ function getTopSuggestion( visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, + subVisualizationId, }).filter(suggestion => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index eabcdfa7a24ab..949ae1f43448e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -44,6 +44,7 @@ export function getSuggestions({ datasourceStates, visualizationMap, activeVisualizationId, + subVisualizationId, visualizationState, field, }: { @@ -57,6 +58,7 @@ export function getSuggestions({ >; visualizationMap: Record<string, Visualization>; activeVisualizationId: string | null; + subVisualizationId?: string; visualizationState: unknown; field?: unknown; }): Suggestion[] { @@ -89,7 +91,8 @@ export function getSuggestions({ table, visualizationId, datasourceSuggestion, - currentVisualizationState + currentVisualizationState, + subVisualizationId ); }) ) @@ -108,13 +111,15 @@ function getVisualizationSuggestions( table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, - currentVisualizationState: unknown + currentVisualizationState: unknown, + subVisualizationId?: string ) { return visualization .getSuggestions({ table, state: currentVisualizationState, keptLayerIds: datasourceSuggestion.keptLayerIds, + subVisualizationId, }) .map(({ state, ...visualizationSuggestion }) => ({ ...visualizationSuggestion, diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 50cd1ad8bd53a..e684fe8b3b5d6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked<Visualization> { label: 'TEST', }, ], + getVisualizationTypeId: jest.fn(_state => 'empty'), getDescription: jest.fn(_state => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), getPersistableState: jest.fn(_state => _state), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts deleted file mode 100644 index 5f35ef650a08c..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { getAutoDate } from './auto_date'; - -describe('auto_date', () => { - let autoDate: ReturnType<typeof getAutoDate>; - - beforeEach(() => { - autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() }); - }); - - it('should do nothing if no time range is provided', () => { - const result = autoDate.fn( - { - type: 'kibana_context', - }, - { - aggConfigs: 'canttouchthis', - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual('canttouchthis'); - }); - - it('should not change anything if there are no auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: '35h' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual(aggConfigs); - }); - - it('should change auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: 'auto' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - const interval = JSON.parse(result).find( - (agg: { type: string }) => agg.type === 'date_histogram' - ).params.interval; - - expect(interval).toBeTruthy(); - expect(typeof interval).toEqual('string'); - expect(interval).not.toEqual('auto'); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts deleted file mode 100644 index 97a46f4a3e176..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { - ExpressionFunctionDefinition, - KibanaContext, -} from '../../../../../src/plugins/expressions/public'; - -interface LensAutoDateProps { - aggConfigs: string; -} - -export function getAutoDate(deps: { - data: DataPublicPluginSetup; -}): ExpressionFunctionDefinition< - 'lens_auto_date', - KibanaContext | null, - LensAutoDateProps, - string -> { - function autoIntervalFromContext(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { - return; - } - - return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange); - } - - /** - * Convert all 'auto' date histograms into a concrete value (e.g. 2h). - * This allows us to support 'auto' on all date fields, and opens the - * door to future customizations (e.g. adjusting the level of detail, etc). - */ - return { - name: 'lens_auto_date', - aliases: [], - help: '', - inputTypes: ['kibana_context', 'null'], - args: { - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - }, - fn(input, args) { - const interval = autoIntervalFromContext(input); - - if (!interval) { - return args.aggConfigs; - } - - const configs = JSON.parse(args.aggConfigs) as Array<{ - type: string; - params: { interval: string }; - }>; - - const updatedConfigs = configs.map(c => { - if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') { - return c; - } - - return { - ...c, - params: { - ...c.params, - interval, - }, - }; - }); - - return JSON.stringify(updatedConfigs); - }, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index fe14f472341af..73fd144b9c7f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; -import { getAutoDate } from './auto_date'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, @@ -31,10 +30,9 @@ export class IndexPatternDatasource { setup( core: CoreSetup<IndexPatternDatasourceStartPlugins>, - { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins ) { expressions.registerFunction(renameColumns); - expressions.registerFunction(getAutoDate({ data: dataSetup })); editorFrame.registerDatasource( core.getStartServices().then(([coreStart, { data }]) => diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e4f3677d0fe88..06635e663361d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { Ast } from '@kbn/interpreter/common'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "aggConfigs": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "aggConfigs": Array [ - "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", - ], - }, - "function": "lens_auto_date", - "type": "function", - }, - ], - "type": "expression", - }, + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", ], "includeFormatHints": Array [ true, @@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => { "partialRows": Array [ false, ], + "timeFields": Array [ + "timestamp", + ], }, "function": "esaggs", "type": "function", @@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => { } `); }); + + it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + col3: { + label: 'Date 2', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'another_datefield', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); + }); + + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count of records', + dataType: 'date', + isBucketed: false, + sourceField: 'timefield', + operationType: 'cardinality', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); + expect(ast.chain[0].arguments.timeFields).not.toContain('timefield'); + }); }); describe('#insertLayer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 3ab51b5fa3f2b..1308fa3b7ca60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; import { OriginalColumn } from './rename_columns'; +import { dateHistogramOperation } from './operations/definitions'; function getExpressionForLayer( indexPattern: IndexPattern, @@ -68,6 +69,12 @@ function getExpressionForLayer( return base; }); + const allDateHistogramFields = Object.values(columns) + .map(column => + column.operationType === dateHistogramOperation.type ? column.sourceField : null + ) + .filter((field): field is string => Boolean(field)); + return { type: 'expression', chain: [ @@ -79,20 +86,8 @@ function getExpressionForLayer( metricsAtAllLevels: [false], partialRows: [false], includeFormatHints: [true], - aggConfigs: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_auto_date', - arguments: { - aggConfigs: [JSON.stringify(aggs)], - }, - }, - ], - }, - ], + timeFields: allDateHistogramFields, + aggConfigs: [JSON.stringify(aggs)], }, }, { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 73b8019a31eaa..04a1c3865f22d 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -53,6 +53,10 @@ export const metricVisualization: Visualization<State, PersistableState> = { }, ], + getVisualizationTypeId() { + return 'lnsMetric'; + }, + clearLayer(state) { return { ...state, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 181f192520d0d..ed0af8545f012 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -312,6 +312,10 @@ export interface SuggestionRequest<T = unknown> { * The visualization needs to know which table is being suggested */ keptLayerIds: string[]; + /** + * Different suggestions can be generated for each subtype of the visualization + */ + subVisualizationId?: string; } /** @@ -388,6 +392,11 @@ export interface Visualization<T = unknown, P = unknown> { * but can register multiple subtypes */ visualizationTypes: VisualizationType[]; + /** + * Return the ID of the current visualization. Used to highlight + * the active subtype of the visualization. + */ + getVisualizationTypeId: (state: T) => string; /** * If the visualization has subtypes, update the subtype in state. */ diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 5dfae097be834..3a280fb045b06 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 e75e5fe763d6a..8db00aba0e36d 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 d6b6de479acfb..85cf5753befd7 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/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index beccf0dc46eb4..d176905c65120 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -27,7 +27,7 @@ function exampleState(): State { } describe('xy_visualization', () => { - describe('getDescription', () => { + describe('#getDescription', () => { function mixedState(...types: SeriesType[]) { const state = exampleState(); return { @@ -81,6 +81,45 @@ describe('xy_visualization', () => { }); }); + describe('#getVisualizationTypeId', () => { + function mixedState(...types: SeriesType[]) { + const state = exampleState(); + return { + ...state, + layers: types.map((t, i) => ({ + ...state.layers[0], + layerId: `layer_${i}`, + seriesType: t, + })), + }; + } + + it('should show mixed when each layer is different', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed'); + }); + + it('should show the preferredSeriesType if there are no layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar'); + }); + + it('should combine multiple layers into one type', () => { + expect( + xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal')) + ).toEqual('bar_horizontal'); + }); + + it('should return the subtype for single layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area'); + expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line'); + expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual( + 'area_stacked' + ); + expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual( + 'bar_horizontal_stacked' + ); + }); + }); + describe('#initialize', () => { it('loads default state', () => { const mockFrame = createMockFramePublicAPI(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index c72fa0fec24d7..e91edf9cc0183 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu } from './xy_config_panel'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; @@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; +function getVisualizationType(state: State): VisualizationType | 'mixed' { + if (!state.layers.length) { + return ( + visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0] + ); + } + const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType); + const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); + + return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; +} + function getDescription(state?: State) { if (!state) { return { @@ -34,32 +46,31 @@ function getDescription(state?: State) { }; } + const visualizationType = getVisualizationType(state); + if (!state.layers.length) { - const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!; + const preferredType = visualizationType as VisualizationType; return { - icon: visualizationType.largeIcon || visualizationType.icon, - label: visualizationType.label, + icon: preferredType.largeIcon || preferredType.icon, + label: preferredType.label, }; } - const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!; - const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); - return { icon: - seriesTypes.length === 1 - ? visualizationType.largeIcon || visualizationType.icon - : chartMixedSVG, + visualizationType === 'mixed' + ? chartMixedSVG + : visualizationType.largeIcon || visualizationType.icon, label: - seriesTypes.length === 1 - ? visualizationType.label - : isHorizontalChart(state.layers) - ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed horizontal bar', - }) - : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY', - }), + visualizationType === 'mixed' + ? isHorizontalChart(state.layers) + ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed horizontal bar', + }) + : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { + defaultMessage: 'Mixed XY', + }) + : visualizationType.label, }; } @@ -67,6 +78,10 @@ export const xyVisualization: Visualization<State, PersistableState> = { id: 'lnsXY', visualizationTypes, + getVisualizationTypeId(state) { + const type = getVisualizationType(state); + return type === 'mixed' ? type : type.id; + }, getLayerIds(state) { return state.layers.map(l => l.layerId); diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index e80308cc9acdb..4cc330d40efd7 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -158,4 +158,124 @@ describe('Lens migrations', () => { ]); }); }); + + describe('7.8.0 auto timestamp', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + type: 'lens', + attributes: { + expression: `kibana + | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" + | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs + index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" + metricsAtAllLevels=false + partialRows=false + includeFormatHints=true + aggConfigs={ + lens_auto_date + aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" + } + | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}" + } + | lens_xy_chart + xTitle="products.created_on" + yTitle="Count of records" + legend={lens_xy_legendConfig isVisible=true position="right"} + layers={lens_xy_layer + layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + hide=false + xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97" + yScaleType="linear" + xScaleType="time" + isHistogram=true + seriesType="bar_stacked" + accessors="66115819-8481-4917-a6dc-8ffb10dd02df" + columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}" + } + `, + state: { + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': { + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + columns: { + '1d9cc16c-1460-41de-88f8-471932ecbc97': { + label: 'products.created_on', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'products.created_on', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '66115819-8481-4917-a6dc-8ffb10dd02df': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + suggestedPriority: 0, + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + }, + }, + columnOrder: [ + '1d9cc16c-1460-41de-88f8-471932ecbc97', + '66115819-8481-4917-a6dc-8ffb10dd02df', + ], + }, + }, + }, + }, + datasourceMetaData: { + filterableIndexPatterns: [ + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c', + accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + title: 'Bar chart', + visualizationType: 'lnsXY', + }, + }; + + it('should remove the lens_auto_date expression', () => { + const result = migrations['7.8.0'](example, context); + expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`); + }); + + it('should handle pre-migrated expression', () => { + const input = { + type: 'lens', + attributes: { + ...example.attributes, + expression: `kibana +| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" +| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"} +| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`, + }, + }; + const result = migrations['7.8.0'](input, context); + expect(result).toEqual(input); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 3d238723b7438..51fcd3b6198c3 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, flow } from 'lodash'; +import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; interface XYLayerPre77 { @@ -14,6 +15,122 @@ interface XYLayerPre77 { accessors: string[]; } +/** + * Removes the `lens_auto_date` subexpression from a stored expression + * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} + */ +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Check for sub-expression in aggConfigs + if ( + node.function === 'esaggs' && + typeof node.arguments.aggConfigs[0] !== 'string' + ) { + return { + ...node, + arguments: { + ...node.arguments, + aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments + .aggConfigs, + }, + }; + } + return node; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + +/** + * Adds missing timeField arguments to esaggs in the Lens expression + */ +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Skip if there are any timeField arguments already, because that indicates + // the fix is already applied + if (node.function !== 'esaggs' || node.arguments.timeFields) { + return node; + } + const timeFields: string[] = []; + JSON.parse(node.arguments.aggConfigs[0] as string).forEach( + (agg: { type: string; params: { field: string } }) => { + if (agg.type !== 'date_histogram') { + return; + } + timeFields.push(agg.params.field); + } + ); + return { + ...node, + arguments: { + ...node.arguments, + timeFields, + }, + }; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + export const migrations: Record<string, SavedObjectMigrationFn> = { '7.7.0': doc => { const newDoc = cloneDeep(doc); @@ -34,4 +151,7 @@ export const migrations: Record<string, SavedObjectMigrationFn> = { } return newDoc; }, + // The order of these migrations matter, since the timefield migration relies on the aggConfigs + // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). + '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs), }; diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index d622e3f71eff5..154692a2fd197 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ import { BehaviorSubject } from 'rxjs'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; +import { featureUsageMock } from './services/feature_usage_service.mock'; -const createSetupMock = () => { +const createSetupMock = (): jest.Mocked<LicensingPluginSetup> => { const license = licenseMock.createLicense(); - const mock: jest.Mocked<LicensingPluginSetup> = { + const mock = { license$: new BehaviorSubject(license), refresh: jest.fn(), createLicensePoller: jest.fn(), + featureUsage: featureUsageMock.createSetup(), }; mock.refresh.mockResolvedValue(license); mock.createLicensePoller.mockReturnValue({ @@ -23,7 +25,16 @@ const createSetupMock = () => { return mock; }; +const createStartMock = (): jest.Mocked<LicensingPluginStart> => { + const mock = { + featureUsage: featureUsageMock.createStart(), + }; + + return mock; +}; + export const licensingMock = { createSetup: createSetupMock, + createStart: createStartMock, ...licenseMock, }; diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 383245e6f4ee8..ee43ac0ce233c 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -20,12 +20,13 @@ import { } from 'src/core/server'; import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; import { ElasticsearchError, RawLicense, RawFeatures } from './types'; import { registerRoutes } from './routes'; +import { FeatureUsageService } from './services'; import { LicenseConfigType } from './licensing_config'; import { createRouteHandlerContext } from './licensing_route_handler_context'; @@ -77,18 +78,19 @@ function sign({ * A plugin for fetching, refreshing, and receiving information about the license for the * current Kibana instance. */ -export class LicensingPlugin implements Plugin<LicensingPluginSetup> { +export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPluginStart, {}, {}> { private stop$ = new Subject(); private readonly logger: Logger; private readonly config$: Observable<LicenseConfigType>; private loggingSubscription?: Subscription; + private featureUsage = new FeatureUsageService(); constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); this.config$ = this.context.config.create<LicenseConfigType>(); } - public async setup(core: CoreSetup) { + public async setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); const config = await this.config$.pipe(take(1)).toPromise(); const pollingFrequency = config.api_polling_frequency; @@ -101,13 +103,14 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> { core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$)); - registerRoutes(core.http.createRouter()); + registerRoutes(core.http.createRouter(), core.getStartServices); core.http.registerOnPreResponse(createOnPreResponseHandler(refresh, license$)); return { refresh, license$, createLicensePoller: this.createLicensePoller.bind(this), + featureUsage: this.featureUsage.setup(), }; } @@ -186,7 +189,11 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> { return error.message; } - public async start(core: CoreStart) {} + public async start(core: CoreStart) { + return { + featureUsage: this.featureUsage.start(), + }; + } public stop() { this.stop$.next(); diff --git a/x-pack/plugins/licensing/server/routes/feature_usage.ts b/x-pack/plugins/licensing/server/routes/feature_usage.ts new file mode 100644 index 0000000000000..5fbfbc3f577b8 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/feature_usage.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { LicensingPluginStart } from '../types'; + +export function registerFeatureUsageRoute( + router: IRouter, + getStartServices: StartServicesAccessor<{}, LicensingPluginStart> +) { + router.get( + { path: '/api/licensing/feature_usage', validate: false }, + async (context, request, response) => { + const [, , { featureUsage }] = await getStartServices(); + return response.ok({ + body: [...featureUsage.getLastUsages().entries()].reduce( + (res, [featureName, lastUsage]) => { + return { + ...res, + [featureName]: new Date(lastUsage).toISOString(), + }; + }, + {} + ), + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/routes/index.ts b/x-pack/plugins/licensing/server/routes/index.ts index 26b3bc6292dd6..2d073a92e507e 100644 --- a/x-pack/plugins/licensing/server/routes/index.ts +++ b/x-pack/plugins/licensing/server/routes/index.ts @@ -3,9 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; + +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { LicensingPluginStart } from '../types'; import { registerInfoRoute } from './info'; +import { registerFeatureUsageRoute } from './feature_usage'; -export function registerRoutes(router: IRouter) { +export function registerRoutes( + router: IRouter, + getStartServices: StartServicesAccessor<{}, LicensingPluginStart> +) { registerInfoRoute(router); + registerFeatureUsageRoute(router, getStartServices); } diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts new file mode 100644 index 0000000000000..f247c6ffcb526 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; + +const createSetupMock = (): jest.Mocked<FeatureUsageServiceSetup> => { + const mock = { + register: jest.fn(), + }; + + return mock; +}; + +const createStartMock = (): jest.Mocked<FeatureUsageServiceStart> => { + const mock = { + notifyUsage: jest.fn(), + getLastUsages: jest.fn(), + }; + + return mock; +}; + +const createServiceMock = (): jest.Mocked<PublicMethodsOf<FeatureUsageService>> => { + const mock = { + setup: jest.fn(() => createSetupMock()), + start: jest.fn(() => createStartMock()), + }; + + return mock; +}; + +export const featureUsageMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts new file mode 100644 index 0000000000000..f0ef0dbec0b22 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FeatureUsageService } from './feature_usage_service'; + +describe('FeatureUsageService', () => { + let service: FeatureUsageService; + + beforeEach(() => { + service = new FeatureUsageService(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const toObj = (map: ReadonlyMap<string, any>): Record<string, any> => + Object.fromEntries(map.entries()); + + describe('#setup', () => { + describe('#register', () => { + it('throws when registering the same feature twice', () => { + const setup = service.setup(); + setup.register('foo'); + expect(() => { + setup.register('foo'); + }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); + }); + }); + }); + + describe('#start', () => { + describe('#notifyUsage', () => { + it('allows to notify a feature usage', () => { + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + start.notifyUsage('feature', 127001); + + expect(start.getLastUsages().get('feature')).toBe(127001); + }); + + it('can receive a Date object', () => { + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + + const usageTime = new Date(2015, 9, 21, 17, 54, 12); + start.notifyUsage('feature', usageTime); + expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime()); + }); + + it('uses the current time when `usedAt` is unspecified', () => { + jest.spyOn(Date, 'now').mockReturnValue(42); + + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + start.notifyUsage('feature'); + + expect(start.getLastUsages().get('feature')).toBe(42); + }); + + it('throws when notifying for an unregistered feature', () => { + service.setup(); + const start = service.start(); + expect(() => { + start.notifyUsage('unregistered'); + }).toThrowErrorMatchingInlineSnapshot(`"Feature 'unregistered' is not registered."`); + }); + }); + + describe('#getLastUsages', () => { + it('returns the last usage for all used features', () => { + const setup = service.setup(); + setup.register('featureA'); + setup.register('featureB'); + const start = service.start(); + start.notifyUsage('featureA', 127001); + start.notifyUsage('featureB', 6666); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 127001, + featureB: 6666, + }); + }); + + it('returns the last usage even after notifying for an older usage', () => { + const setup = service.setup(); + setup.register('featureA'); + const start = service.start(); + start.notifyUsage('featureA', 1000); + start.notifyUsage('featureA', 500); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 1000, + }); + }); + + it('does not return entries for unused registered features', () => { + const setup = service.setup(); + setup.register('featureA'); + setup.register('featureB'); + const start = service.start(); + start.notifyUsage('featureA', 127001); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 127001, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts new file mode 100644 index 0000000000000..47ffe3a3d9f54 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isDate } from 'lodash'; + +/** @public */ +export interface FeatureUsageServiceSetup { + /** + * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. + */ + register(featureName: string): void; +} + +/** @public */ +export interface FeatureUsageServiceStart { + /** + * Notify of a registered feature usage at given time. + * + * @param featureName - the name of the feature to notify usage of + * @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. + */ + notifyUsage(featureName: string, usedAt?: Date | number): void; + /** + * Return a map containing last usage timestamp for all features. + * Features that were not used yet do not appear in the map. + */ + getLastUsages(): ReadonlyMap<string, number>; +} + +export class FeatureUsageService { + private readonly features: string[] = []; + private readonly lastUsages = new Map<string, number>(); + + public setup(): FeatureUsageServiceSetup { + return { + register: featureName => { + if (this.features.includes(featureName)) { + throw new Error(`Feature '${featureName}' has already been registered.`); + } + this.features.push(featureName); + }, + }; + } + + public start(): FeatureUsageServiceStart { + return { + notifyUsage: (featureName, usedAt = Date.now()) => { + if (!this.features.includes(featureName)) { + throw new Error(`Feature '${featureName}' is not registered.`); + } + if (isDate(usedAt)) { + usedAt = usedAt.getTime(); + } + const currentValue = this.lastUsages.get(featureName) ?? 0; + this.lastUsages.set(featureName, Math.max(usedAt, currentValue)); + }, + getLastUsages: () => new Map(this.lastUsages.entries()), + }; + } +} diff --git a/x-pack/plugins/licensing/server/services/index.ts b/x-pack/plugins/licensing/server/services/index.ts new file mode 100644 index 0000000000000..fc890dd3c927d --- /dev/null +++ b/x-pack/plugins/licensing/server/services/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index f46167a0d0a42..f11d9d5e69a58 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { IClusterClient } from 'src/core/server'; import { ILicense, LicenseStatus, LicenseType } from '../common/types'; +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; export interface ElasticsearchError extends Error { status?: number; @@ -57,7 +58,6 @@ export interface LicensingPluginSetup { * Triggers licensing information re-fetch. */ refresh(): Promise<ILicense>; - /** * Creates a license poller to retrieve a license data with. * Allows a plugin to configure a cluster to retrieve data from at @@ -67,4 +67,16 @@ export interface LicensingPluginSetup { clusterClient: IClusterClient, pollingFrequency: number ) => { license$: Observable<ILicense>; refresh(): Promise<ILicense> }; + /** + * APIs to register licensed feature usage. + */ + featureUsage: FeatureUsageServiceSetup; +} + +/** @public */ +export interface LicensingPluginStart { + /** + * APIs to manage licensed feature usage. + */ + featureUsage: FeatureUsageServiceStart; } diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts new file mode 100644 index 0000000000000..dbe31fed66413 --- /dev/null +++ b/x-pack/plugins/lists/common/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Lists routes + */ +export const LIST_URL = `/api/lists`; +export const LIST_INDEX = `${LIST_URL}/index`; +export const LIST_ITEM_URL = `${LIST_URL}/items`; diff --git a/x-pack/plugins/lists/common/schemas/common/index.ts b/x-pack/plugins/lists/common/schemas/common/index.ts new file mode 100644 index 0000000000000..a05e97ded38ee --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './schemas'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts new file mode 100644 index 0000000000000..edc037ed7a0b1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '../types/non_empty_string'; + +export const name = t.string; +export type Name = t.TypeOf<typeof name>; +export const nameOrUndefined = t.union([name, t.undefined]); +export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>; + +export const description = t.string; +export type Description = t.TypeOf<typeof description>; +export const descriptionOrUndefined = t.union([description, t.undefined]); +export type DescriptionOrUndefined = t.TypeOf<typeof descriptionOrUndefined>; + +export const list_id = NonEmptyString; +export const list_idOrUndefined = t.union([list_id, t.undefined]); +export type List_idOrUndefined = t.TypeOf<typeof list_idOrUndefined>; + +export const item = t.string; +export const created_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_by = t.string; +export const created_by = t.string; +export const file = t.object; + +export const id = NonEmptyString; +export type Id = t.TypeOf<typeof id>; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf<typeof idOrUndefined>; + +export const ip = t.string; +export const ipOrUndefined = t.union([ip, t.undefined]); + +export const keyword = t.string; +export const keywordOrUndefined = t.union([keyword, t.undefined]); + +export const value = t.string; +export const valueOrUndefined = t.union([value, t.undefined]); + +export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation +export const _index = t.string; + +export const type = t.keyof({ ip: null, keyword: null }); // TODO: Add the other data types here + +export const typeOrUndefined = t.union([type, t.undefined]); +export type Type = t.TypeOf<typeof type>; + +export const meta = t.object; +export type Meta = t.TypeOf<typeof meta>; +export const metaOrUndefined = t.union([meta, t.undefined]); +export type MetaOrUndefined = t.TypeOf<typeof metaOrUndefined>; + +export const esDataTypeUnion = t.union([t.type({ ip }), t.type({ keyword })]); +export type EsDataTypeUnion = t.TypeOf<typeof esDataTypeUnion>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts new file mode 100644 index 0000000000000..4a825382c06e4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { _index } from '../common/schemas'; + +export const createEsBulkTypeSchema = t.exact( + t.type({ + create: t.exact(t.type({ _index })), + }) +); + +export type CreateEsBulkTypeSchema = t.TypeOf<typeof createEsBulkTypeSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts new file mode 100644 index 0000000000000..d70dd09849fa6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './update_es_list_schema'; +export * from './index_es_list_schema'; +export * from './update_es_list_item_schema'; +export * from './index_es_list_item_schema'; +export * from './create_es_bulk_type'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts new file mode 100644 index 0000000000000..596498b64b771 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + esDataTypeUnion, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListItemSchema = t.intersection([ + t.exact( + t.type({ + created_at, + created_by, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type IndexEsListItemSchema = t.TypeOf<typeof indexEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts new file mode 100644 index 0000000000000..e0924392628a9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type IndexEsListSchema = t.TypeOf<typeof indexEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts new file mode 100644 index 0000000000000..e4cf46bc39429 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { esDataTypeUnion, metaOrUndefined, updated_at, updated_by } from '../common/schemas'; + +export const updateEsListItemSchema = t.intersection([ + t.exact( + t.type({ + meta: metaOrUndefined, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type UpdateEsListItemSchema = t.TypeOf<typeof updateEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts new file mode 100644 index 0000000000000..8f23f3744e563 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + descriptionOrUndefined, + metaOrUndefined, + nameOrUndefined, + updated_at, + updated_by, +} from '../common/schemas'; + +export const updateEsListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + meta: metaOrUndefined, + name: nameOrUndefined, + updated_at, + updated_by, + }) +); + +export type UpdateEsListSchema = t.TypeOf<typeof updateEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/index.ts b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts new file mode 100644 index 0000000000000..6fbc6ef293064 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './search_es_list_item_schema'; +export * from './search_es_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts new file mode 100644 index 0000000000000..902d3e6a9896e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + ipOrUndefined, + keywordOrUndefined, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListItemSchema = t.exact( + t.type({ + created_at, + created_by, + ip: ipOrUndefined, + keyword: keywordOrUndefined, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) +); + +export type SearchEsListItemSchema = t.TypeOf<typeof searchEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts new file mode 100644 index 0000000000000..00a7c6f321d38 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type SearchEsListSchema = t.TypeOf<typeof searchEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/index.ts b/x-pack/plugins/lists/common/schemas/index.ts new file mode 100644 index 0000000000000..6a60a6df55691 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './common'; +export * from './request'; +export * from './response'; +export * from './elastic_query'; +export * from './elastic_response'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts new file mode 100644 index 0000000000000..8168e5a9838f2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_id, metaOrUndefined, value } from '../common/schemas'; + +export const createListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id, + meta: metaOrUndefined, + value, + }) +); + +export type CreateListItemSchema = t.TypeOf<typeof createListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts new file mode 100644 index 0000000000000..ba791a55d17eb --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListRequest } from './mocks/utils'; +import { createListSchema } from './create_list_schema'; + +describe('create_list_schema', () => { + // TODO: Finish the tests for this + test('it should validate a typical lists request', () => { + const payload = getListRequest(); + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + description: 'Description of a list item', + id: 'some-list-id', + name: 'Name of a list item', + type: 'ip', + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts new file mode 100644 index 0000000000000..353a4ecdafa0c --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, idOrUndefined, metaOrUndefined, name, type } from '../common/schemas'; + +export const createListSchema = t.exact( + t.type({ + description, + id: idOrUndefined, + meta: metaOrUndefined, + name, + type, + }) +); + +export type CreateListSchema = t.TypeOf<typeof createListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts new file mode 100644 index 0000000000000..f4c1fb5c43eb0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const deleteListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id: list_idOrUndefined, + value: valueOrUndefined, + }) +); + +export type DeleteListItemSchema = t.TypeOf<typeof deleteListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts new file mode 100644 index 0000000000000..fd6aa5b85f81a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const deleteListSchema = t.exact( + t.type({ + id, + }) +); + +export type DeleteListSchema = t.TypeOf<typeof deleteListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts new file mode 100644 index 0000000000000..14b201bf8089d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_id } from '../common/schemas'; + +export const exportListItemQuerySchema = t.exact( + t.type({ + list_id, + // TODO: Add file_name here with a default value + }) +); + +export type ExportListItemQuerySchema = t.TypeOf<typeof exportListItemQuerySchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts new file mode 100644 index 0000000000000..b8467d141bdd8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_idOrUndefined, typeOrUndefined } from '../common/schemas'; + +export const importListItemQuerySchema = t.exact( + t.type({ list_id: list_idOrUndefined, type: typeOrUndefined }) +); + +export type ImportListItemQuerySchema = t.TypeOf<typeof importListItemQuerySchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts new file mode 100644 index 0000000000000..0cf01db8617f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import { Readable } from 'stream'; + +import * as t from 'io-ts'; + +import { file } from '../common/schemas'; + +export const importListItemSchema = t.exact( + t.type({ + file, + }) +); + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} + +/** + * Special interface since we are streaming in a file through a reader + */ +export interface ImportListItemSchema { + file: HapiReadableStream; +} diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts new file mode 100644 index 0000000000000..d332ab1eb1bab --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_list_item_schema'; +export * from './create_list_schema'; +export * from './delete_list_item_schema'; +export * from './delete_list_schema'; +export * from './export_list_item_query_schema'; +export * from './import_list_item_schema'; +export * from './patch_list_item_schema'; +export * from './patch_list_schema'; +export * from './read_list_item_schema'; +export * from './read_list_schema'; +export * from './import_list_item_query_schema'; +export * from './update_list_schema'; +export * from './update_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts new file mode 100644 index 0000000000000..e5d189db8490b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CreateListSchema } from '../create_list_schema'; + +export const getListRequest = (): CreateListSchema => ({ + description: 'Description of a list item', + id: 'some-list-id', + meta: undefined, + name: 'Name of a list item', + type: 'ip', +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts new file mode 100644 index 0000000000000..3e8198a5109b3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const patchListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value: valueOrUndefined, + }) +); + +export type PatchListItemSchema = t.TypeOf<typeof patchListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts new file mode 100644 index 0000000000000..efcb81fc8be2a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { descriptionOrUndefined, id, metaOrUndefined, nameOrUndefined } from '../common/schemas'; + +export const patchListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + id, + meta: metaOrUndefined, + name: nameOrUndefined, + }) +); + +export type PatchListSchema = t.TypeOf<typeof patchListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts new file mode 100644 index 0000000000000..9ea14a2a21ed8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const readListItemSchema = t.exact( + t.type({ id: idOrUndefined, list_id: list_idOrUndefined, value: valueOrUndefined }) +); + +export type ReadListItemSchema = t.TypeOf<typeof readListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts new file mode 100644 index 0000000000000..8803346709c31 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const readListSchema = t.exact( + t.type({ + id, + }) +); + +export type ReadListSchema = t.TypeOf<typeof readListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts new file mode 100644 index 0000000000000..e1f88bae66e0f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, value } from '../common/schemas'; + +export const updateListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value, + }) +); + +export type UpdateListItemSchema = t.TypeOf<typeof updateListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts new file mode 100644 index 0000000000000..d51ed60c41b56 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, id, metaOrUndefined, name } from '../common/schemas'; + +export const updateListSchema = t.exact( + t.type({ + description, + id, + meta: metaOrUndefined, + name, + }) +); + +export type UpdateListSchema = t.TypeOf<typeof updateListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts new file mode 100644 index 0000000000000..55aaf587ac06b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const acknowledgeSchema = t.type({ acknowledged: t.boolean }); + +export type AcknowledgeSchema = t.TypeOf<typeof acknowledgeSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts new file mode 100644 index 0000000000000..3f11adf58d8d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './list_item_schema'; +export * from './list_schema'; +export * from './acknowledge_schema'; +export * from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts new file mode 100644 index 0000000000000..bf2bf21d2c216 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const listItemIndexExistSchema = t.type({ + list_index: t.boolean, + list_item_index: t.boolean, +}); + +export type ListItemIndexExistSchema = t.TypeOf<typeof listItemIndexExistSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts new file mode 100644 index 0000000000000..6c2f2ed9a7095 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +/* eslint-disable @typescript-eslint/camelcase */ + +import { + created_at, + created_by, + id, + list_id, + metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, +} from '../common/schemas'; + +export const listItemSchema = t.exact( + t.type({ + created_at, + created_by, + id, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, + }) +); + +export type ListItemSchema = t.TypeOf<typeof listItemSchema>; + +export const listItemArraySchema = t.array(listItemSchema); +export type ListItemArraySchema = t.TypeOf<typeof listItemArraySchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts new file mode 100644 index 0000000000000..cad449766ceb4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + id, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const listSchema = t.exact( + t.type({ + created_at, + created_by, + description, + id, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type ListSchema = t.TypeOf<typeof listSchema>; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts new file mode 100644 index 0000000000000..d1e2094bbcad3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export type NonEmptyStringC = t.Type<string, string, unknown>; + +/** + * Types the NonEmptyString as: + * - A string that is not empty + */ +export const NonEmptyString: NonEmptyStringC = new t.Type<string, string, unknown>( + 'NonEmptyString', + t.string.is, + (input, context): Either<t.Errors, string> => { + if (typeof input === 'string' && input.trim() !== '') { + return t.success(input); + } else { + return t.failure(input, context); + } + }, + t.identity +); diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts new file mode 100644 index 0000000000000..5e74753a6f0bd --- /dev/null +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getPaths, foldLeftRight } from '../../siem/server/utils/build_validation/__mocks__/utils'; +export { exactCheck } from '../../siem/server/utils/build_validation/exact_check'; diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json new file mode 100644 index 0000000000000..b7aaac6d3fc76 --- /dev/null +++ b/x-pack/plugins/lists/kibana.json @@ -0,0 +1,10 @@ +{ + "configPath": ["xpack", "lists"], + "id": "lists", + "kibanaVersion": "kibana", + "requiredPlugins": [], + "optionalPlugins": ["spaces", "security"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts new file mode 100644 index 0000000000000..3e7995b2ce8d0 --- /dev/null +++ b/x-pack/plugins/lists/server/config.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + listIndex: schema.string({ defaultValue: '.lists' }), + listItemIndex: schema.string({ defaultValue: '.items' }), +}); + +export type ConfigType = TypeOf<typeof ConfigSchema>; diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts new file mode 100644 index 0000000000000..3158fabda935f --- /dev/null +++ b/x-pack/plugins/lists/server/create_config.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { map } from 'rxjs/operators'; +import { PluginInitializerContext } from 'kibana/server'; +import { Observable } from 'rxjs'; + +import { ConfigType } from './config'; + +export const createConfig$ = ( + context: PluginInitializerContext +): Observable<Readonly<{ + enabled: boolean; + listIndex: string; + listItemIndex: string; +}>> => { + return context.config.create<ConfigType>().pipe(map(config => config)); +}; diff --git a/x-pack/plugins/lists/server/error_with_status_code.ts b/x-pack/plugins/lists/server/error_with_status_code.ts new file mode 100644 index 0000000000000..f9bbbc4abad27 --- /dev/null +++ b/x-pack/plugins/lists/server/error_with_status_code.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class ErrorWithStatusCode extends Error { + private readonly statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } + + public getStatusCode = (): number => this.statusCode; +} diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts new file mode 100644 index 0000000000000..c1e577aa60195 --- /dev/null +++ b/x-pack/plugins/lists/server/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; + +import { ConfigSchema } from './config'; +import { ListPlugin } from './plugin'; + +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => + new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts new file mode 100644 index 0000000000000..4473d68d3c646 --- /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 0000000000000..1c893fb757c5d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { LIST_INDEX } from '../../common/constants'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListIndexRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" already exists`, + statusCode: 409, + }); + } else { + const policyExists = await lists.getListPolicyExists(); + const policyListItemExists = await lists.getListItemPolicyExists(); + + if (!policyExists) { + await lists.setListPolicy(); + } + if (!policyListItemExists) { + await lists.setListItemPolicy(); + } + + const templateExists = await lists.getListTemplateExists(); + const templateListItemsExists = await lists.getListItemTemplateExists(); + + if (!templateExists) { + await lists.setListTemplate(); + } + + if (!templateListItemsExists) { + await lists.setListItemTemplate(); + } + + if (!listIndexExists) { + await lists.createListBootStrapIndex(); + } + if (!listItemIndexExists) { + await lists.createListItemBootStrapIndex(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts new file mode 100644 index 0000000000000..68622e98cbc52 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListItemSchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(createListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value, meta } = request.body; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ listId, type: list.type, value }); + if (listItem.length !== 0) { + return siemResponse.error({ + body: `list_id: "${listId}" already contains the given value: ${value}`, + statusCode: 409, + }); + } else { + const createdListItem = await lists.createListItem({ + id, + listId, + meta, + type: list.type, + value, + }); + const [validated, errors] = validate(createdListItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts new file mode 100644 index 0000000000000..0f3c404c53cfd --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(createListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, type, meta } = request.body; + const lists = getListClient(context); + const listExists = await lists.getListIndexExists(); + if (!listExists) { + return siemResponse.error({ + body: `To create a list, the index must exist first. Index "${lists.getListIndex()}" does not exist`, + statusCode: 400, + }); + } else { + if (id != null) { + const list = await lists.getList({ id }); + if (list != null) { + return siemResponse.error({ + body: `list id: "${id}" already exists`, + statusCode: 409, + }); + } + } + const list = await lists.createList({ description, id, meta, name, type }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts new file mode 100644 index 0000000000000..424c3f45aac40 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +/** + * Deletes all of the indexes, template, ilm policies, and aliases. You can check + * this by looking at each of these settings from ES after a deletion: + * + * GET /_template/.lists-default + * GET /.lists-default-000001/ + * GET /_ilm/policy/.lists-default + * GET /_alias/.lists-default + * + * GET /_template/.items-default + * GET /.items-default-000001/ + * GET /_ilm/policy/.items-default + * GET /_alias/.items-default + * + * And ensuring they're all gone + */ +export const deleteListIndexRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (!listIndexExists && !listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" does not exist`, + statusCode: 404, + }); + } else { + if (listIndexExists) { + await lists.deleteListIndex(); + } + if (listItemIndexExists) { + await lists.deleteListItemIndex(); + } + + const listsPolicyExists = await lists.getListPolicyExists(); + const listItemPolicyExists = await lists.getListItemPolicyExists(); + + if (listsPolicyExists) { + await lists.deleteListPolicy(); + } + if (listItemPolicyExists) { + await lists.deleteListItemPolicy(); + } + + const listsTemplateExists = await lists.getListTemplateExists(); + const listItemTemplateExists = await lists.getListItemTemplateExists(); + + if (listsTemplateExists) { + await lists.deleteListTemplate(); + } + if (listItemTemplateExists) { + await lists.deleteListItemTemplate(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts new file mode 100644 index 0000000000000..51b4eb9f02cc2 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListItemRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(deleteListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const deleted = await lists.deleteListItem({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list item with id: "${id}" item not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const deleted = await lists.deleteListItemByValue({ listId, type: list.type, value }); + if (deleted == null || deleted.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" with ${value} was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts new file mode 100644 index 0000000000000..e89355b7689c5 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(deleteListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = getListClient(context); + const { id } = request.query; + const deleted = await lists.deleteList({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list id: "${id}" was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts new file mode 100644 index 0000000000000..32b99bfc512bf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Stream } from 'stream'; + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { exportListItemQuerySchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const exportListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_export`, + validate: { + query: buildRouteValidation(exportListItemQuerySchema), + // TODO: Do we want to add a body here like export_rules_route and allow a size limit? + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: ${listId} does not exist`, + statusCode: 400, + }); + } else { + // TODO: Allow the API to override the name of the file to export + const fileName = list.name; + + const stream = new Stream.PassThrough(); + lists.exportListItemsToStream({ listId, stream, stringToAppend: '\n' }); + return response.ok({ + body: stream, + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'text/plain', + }, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts new file mode 100644 index 0000000000000..a3b6a520a4ecf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { + ImportListItemSchema, + importListItemQuerySchema, + importListItemSchema, + listSchema, +} from '../../common/schemas'; + +import { getListClient } from '.'; + +export const importListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + body: { + output: 'stream', + }, + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_import`, + validate: { + body: buildRouteValidation<typeof importListItemSchema, ImportListItemSchema>( + importListItemSchema + ), + query: buildRouteValidation(importListItemQuerySchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId, type } = request.query; + const lists = getListClient(context); + if (listId != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 409, + }); + } + await lists.importListItemsToStream({ + listId, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (type != null) { + const { filename } = request.body.file.hapi; + // TODO: Should we prevent the same file from being uploaded multiple times? + const list = await lists.createListIfItDoesNotExist({ + description: `File uploaded from file system of ${filename}`, + id: filename, + meta: undefined, + name: filename, + type, + }); + await lists.importListItemsToStream({ + listId: list.id, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { + return siemResponse.error({ + body: 'Either type or list_id need to be defined in the query', + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts new file mode 100644 index 0000000000000..4951cddc56939 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_list_index_route'; +export * from './create_list_item_route'; +export * from './create_list_route'; +export * from './delete_list_index_route'; +export * from './delete_list_item_route'; +export * from './delete_list_route'; +export * from './export_list_item_route'; +export * from './import_list_item_route'; +export * from './init_routes'; +export * from './patch_list_item_route'; +export * from './patch_list_route'; +export * from './read_list_index_route'; +export * from './read_list_item_route'; +export * from './read_list_route'; +export * from './utils'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts new file mode 100644 index 0000000000000..924dd086ee708 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { updateListRoute } from './update_list_route'; +import { updateListItemRoute } from './update_list_item_route'; + +import { + createListIndexRoute, + createListItemRoute, + createListRoute, + deleteListIndexRoute, + deleteListItemRoute, + deleteListRoute, + exportListItemRoute, + importListItemRoute, + patchListItemRoute, + patchListRoute, + readListIndexRoute, + readListItemRoute, + readListRoute, +} from '.'; + +export const initRoutes = (router: IRouter): void => { + // lists + createListRoute(router); + readListRoute(router); + updateListRoute(router); + deleteListRoute(router); + patchListRoute(router); + + // lists items + createListItemRoute(router); + readListItemRoute(router); + updateListItemRoute(router); + deleteListItemRoute(router); + patchListItemRoute(router); + exportListItemRoute(router); + importListItemRoute(router); + + // indexes of lists + createListIndexRoute(router); + readListIndexRoute(router); + deleteListIndexRoute(router); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts new file mode 100644 index 0000000000000..e18fd0618b133 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, patchListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListItemRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(patchListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts new file mode 100644 index 0000000000000..9d3fa4db8ccd0 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, patchListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(patchListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts new file mode 100644 index 0000000000000..248fc72666d70 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { listItemIndexExistSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListIndexRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists || listItemIndexExists) { + const [validated, errors] = validate( + { list_index: listIndexExists, lists_item_index: listItemIndexExists }, + listItemIndexExistSchema + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (!listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListIndex()} does not exist`, + statusCode: 404, + }); + } else if (!listItemIndexExists && listIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `index ${lists.getListIndex()} and index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts new file mode 100644 index 0000000000000..0a60cba786f04 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(readListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const listItem = await lists.getListItem({ id }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ + listId, + type: list.type, + value, + }); + if (listItem.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" item of ${value} does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts new file mode 100644 index 0000000000000..c30eadfca0b65 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, readListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(readListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts new file mode 100644 index 0000000000000..494d57b93b8e4 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, updateListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListItemRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(updateListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts new file mode 100644 index 0000000000000..6ace61e46a780 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, updateListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(updateListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/utils/get_list_client.ts b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts new file mode 100644 index 0000000000000..a16163ec0fa3a --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'kibana/server'; + +import { ListClient } from '../../services/lists/client'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export const getListClient = (context: RequestHandlerContext): ListClient => { + const lists = context.lists?.getListClient(); + if (lists == null) { + throw new ErrorWithStatusCode('Lists is not found as a plugin', 404); + } else { + return lists; + } +}; diff --git a/x-pack/plugins/lists/server/routes/utils/index.ts b/x-pack/plugins/lists/server/routes/utils/index.ts new file mode 100644 index 0000000000000..a601bdfc003c5 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './get_list_client'; diff --git a/x-pack/plugins/lists/server/scripts/check_env_variables.sh b/x-pack/plugins/lists/server/scripts/check_env_variables.sh new file mode 100755 index 0000000000000..fb3bbbe0fad18 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/check_env_variables.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +# Add this to the start of any scripts to detect if env variables are set + +set -e + +if [ -z "${ELASTICSEARCH_USERNAME}" ]; then + echo "Set ELASTICSEARCH_USERNAME in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_PASSWORD}" ]; then + echo "Set ELASTICSEARCH_PASSWORD in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_URL}" ]; then + echo "Set ELASTICSEARCH_URL in your environment" + exit 1 +fi + +if [ -z "${KIBANA_URL}" ]; then + echo "Set KIBANA_URL in your environment" + exit 1 +fi + +if [ -z "${TASK_MANAGER_INDEX}" ]; then + echo "Set TASK_MANAGER_INDEX in your environment" + exit 1 +fi + +if [ -z "${KIBANA_INDEX}" ]; then + echo "Set KIBANA_INDEX in your environment" + exit 1 +fi diff --git a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh new file mode 100755 index 0000000000000..5b65bb14414c7 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_all_lists.sh +# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + + +# Delete all the main lists that have children items +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list" } + } + }' \ + | jq . + +# Delete all the list children items as well +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list_item" } + } + }' \ + | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list.sh b/x-pack/plugins/lists/server/scripts/delete_list.sh new file mode 100755 index 0000000000000..9934ce61c7107 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_by_list_id.sh ${list_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_index.sh b/x-pack/plugins/lists/server/scripts/delete_list_index.sh new file mode 100755 index 0000000000000..85f06ffbd6670 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_index.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_signal_index.sh +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh new file mode 100755 index 0000000000000..ab14d8c8a80ed --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_id.sh?id={id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1 | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh new file mode 100755 index 0000000000000..6d3213ccb8793 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_value.sh?list_id=${some_id}&value=${some_ip} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/export_list_items.sh b/x-pack/plugins/lists/server/scripts/export_list_items.sh new file mode 100755 index 0000000000000..ba355854c77cc --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-ips.txt} + +# Example to export +# ./export_list_items.sh > /tmp/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=${LIST_ID}" diff --git a/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh new file mode 100755 index 0000000000000..5efad01e9a68e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +FOLDER=${1:-/tmp} + +# Example to export +# ./export_list_items_to_file.sh + +# Change current working directory as exports cause Kibana to restart +pushd ${FOLDER} > /dev/null + +curl -s -k -OJ \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=list-ip" + +popd > /dev/null diff --git a/x-pack/plugins/lists/server/scripts/get_list.sh b/x-pack/plugins/lists/server/scripts/get_list.sh new file mode 100755 index 0000000000000..7f0e4e3062266 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list.sh {list_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh new file mode 100755 index 0000000000000..31d26e195815f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_id ${id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh new file mode 100755 index 0000000000000..24ca27b0c949d --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_value.sh ${list_id} ${value} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/hard_reset.sh b/x-pack/plugins/lists/server/scripts/hard_reset.sh new file mode 100755 index 0000000000000..861928866369b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/hard_reset.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# re-create the list and list item indexes +./delete_list_index.sh +./post_list_index.sh diff --git a/x-pack/plugins/lists/server/scripts/import_list_items.sh b/x-pack/plugins/lists/server/scripts/import_list_items.sh new file mode 100755 index 0000000000000..a39409cd08267 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-list-ip} +FILE=${2:-./lists/files/ips.txt} + +# ./import_list_items.sh list-ip ./lists/files/ips.txt +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?list_id=${LIST_ID}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh new file mode 100755 index 0000000000000..4ec55cb4c5f7b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +TYPE=${1:-ip} +FILE=${2:-./lists/files/ips.txt} + +# Example to import ips from ./lists/files/ips.txt +# ./import_list_items_by_filename.sh ip ./lists/files/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?type=${TYPE}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt new file mode 100644 index 0000000000000..aee32e3a4bd92 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt @@ -0,0 +1,2 @@ +kibana +rock01 diff --git a/x-pack/plugins/lists/server/scripts/lists/files/ips.txt b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt new file mode 100644 index 0000000000000..cf8ebcacae5a1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt @@ -0,0 +1,9 @@ +127.0.0.1 +127.0.0.2 +127.0.0.3 +127.0.0.4 +127.0.0.5 +127.0.0.6 +127.0.0.7 +127.0.0.8 +127.0.0.9 diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json new file mode 100644 index 0000000000000..196b3b149ab82 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json @@ -0,0 +1,13 @@ +{ + "id": "list-ip-everything", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json new file mode 100644 index 0000000000000..3e12ef1754f07 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json @@ -0,0 +1,6 @@ +{ + "id": "list-ip", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json new file mode 100644 index 0000000000000..1516fa5057e50 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json @@ -0,0 +1,5 @@ +{ + "id": "hand_inserted_item_id", + "list_id": "list-ip", + "value": "127.0.0.1" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json new file mode 100644 index 0000000000000..9730c1b7523f1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json @@ -0,0 +1,12 @@ +{ + "id": "hand_inserted_item_id_everything", + "list_id": "list-ip", + "value": "127.0.0.2", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json new file mode 100644 index 0000000000000..4a95a62b67c3e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json @@ -0,0 +1,5 @@ +{ + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json new file mode 100644 index 0000000000000..e8f5fa7e38a06 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json @@ -0,0 +1,6 @@ +{ + "id": "list-keyword", + "name": "Simple list with a keyword", + "description": "This list describes bad host names", + "type": "keyword" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json new file mode 100644 index 0000000000000..b736e7b96ad98 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json @@ -0,0 +1,4 @@ +{ + "list_id": "list-keyword", + "value": "kibana" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json new file mode 100644 index 0000000000000..1a57ab8b6a3b9 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json @@ -0,0 +1,4 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json new file mode 100644 index 0000000000000..936a070ede52c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json @@ -0,0 +1,5 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else", + "description": "Some other description here for you" +} diff --git a/x-pack/plugins/lists/server/scripts/lists_index_exists.sh b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh new file mode 100755 index 0000000000000..7dfbd5b1bada5 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./lists_index_exists.sh +curl -s -k -f \ + -H 'Content-Type: application/json' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/patch_list.sh b/x-pack/plugins/lists/server/scripts/patch_list.sh new file mode 100755 index 0000000000000..3a517a52dbd21 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/simplest_updated_name.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/simplest_updated_name.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/patch_list_item.sh b/x-pack/plugins/lists/server/scripts/patch_list_item.sh new file mode 100755 index 0000000000000..406b03dc6499c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list.sh b/x-pack/plugins/lists/server/scripts/post_list.sh new file mode 100755 index 0000000000000..6aaffee0bc4b2 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list_index.sh b/x-pack/plugins/lists/server/scripts/post_list_index.sh new file mode 100755 index 0000000000000..b7c372d3947e3 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_index.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./post_signal_index.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/post_list_item.sh b/x-pack/plugins/lists/server/scripts/post_list_item.sh new file mode 100755 index 0000000000000..b55a60420674f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip_item.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list.sh b/x-pack/plugins/lists/server/scripts/update_list.sh new file mode 100755 index 0000000000000..4d93544d568a8 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/simple_update.json}) + +# Example: ./update_list.sh +# Example: ./update_list.sh ./lists/updates/simple_update.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list_item.sh b/x-pack/plugins/lists/server/scripts/update_list_item.sh new file mode 100755 index 0000000000000..e3153bfd25b19 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/updates/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts new file mode 100644 index 0000000000000..946e1c240be31 --- /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 0000000000000..fd8fe7077fd58 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import readLine from 'readline'; +import { Readable } from 'stream'; + +const BUFFER_SIZE = 100; + +export class BufferLines extends Readable { + private set = new Set<string>(); + constructor({ input }: { input: NodeJS.ReadableStream }) { + super({ encoding: 'utf-8' }); + const readline = readLine.createInterface({ + input, + }); + + readline.on('line', line => { + this.push(line); + }); + + readline.on('close', () => { + this.push(null); + }); + } + + public _read(): void { + // No operation but this is required to be implemented + } + + public push(line: string | null): boolean { + if (line == null) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + this.emit('close'); + return true; + } else { + this.set.add(line); + if (this.set.size > BUFFER_SIZE) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + return true; + } else { + return true; + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts new file mode 100644 index 0000000000000..b2bca241c468c --- /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 0000000000000..da1e192bf2412 --- /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 0000000000000..9263b975b20e7 --- /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 0000000000000..7100a5f8eaabc --- /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 0000000000000..795c579462b69 --- /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 0000000000000..ffce2d3b2af81 --- /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 0000000000000..dee890445f9a3 --- /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 0000000000000..f2f5ec3078e62 --- /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 0000000000000..937993f1d8f71 --- /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 0000000000000..1c91b69801648 --- /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 0000000000000..d30b3c795550f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemByValueOptionsMocks, getListItemResponseMock } from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; +import { getListItemByValue } from './get_list_item_by_value'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('get_list_by_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Calls get_list_item_by_values with its input', async () => { + const listItemMock = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([listItemMock]); + const options = getListItemByValueOptionsMocks(); + const listItem = await getListItemByValue(options); + const expected = getListItemResponseMock(); + expect(listItem).toEqual([expected]); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts new file mode 100644 index 0000000000000..a6efcbc0d3ffb --- /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 0000000000000..55b170487d95a --- /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 0000000000000..1e5c0b4a6655c --- /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 0000000000000..0ea8320e966bd --- /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 0000000000000..c9f1bfd4d44e4 --- /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 0000000000000..9c85fa6ff0256 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemTemplate } from './get_list_item_template'; + +jest.mock('./list_item_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_item_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListItemTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_template.ts b/x-pack/plugins/lists/server/services/items/get_list_item_template.ts new file mode 100644 index 0000000000000..95f4a09b40648 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import listsItemsMappings from './list_item_mappings.json'; + +export const getListItemTemplate = (index: string): Record<string, unknown> => { + const template = { + index_patterns: [`${index}-*`], + mappings: listsItemsMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, + }; + return template; +}; diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts new file mode 100644 index 0000000000000..ee1d83fabca31 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './buffer_lines'; +export * from './create_list_item'; +export * from './create_list_items_bulk'; +export * from './delete_list_item_by_value'; +export * from './get_list_item_by_value'; +export * from './get_list_item'; +export * from './get_list_item_by_values'; +export * from './update_list_item'; +export * from './write_lines_to_bulk_list_items'; +export * from './write_list_items_to_stream'; +export * from './get_list_item_template'; +export * from './delete_list_item'; +export * from './get_list_item_index'; diff --git a/x-pack/plugins/lists/server/services/items/list_item_mappings.json b/x-pack/plugins/lists/server/services/items/list_item_mappings.json new file mode 100644 index 0000000000000..ca69c26df52b5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "tie_breaker_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "keyword": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/list_item_policy.json b/x-pack/plugins/lists/server/services/items/list_item_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts new file mode 100644 index 0000000000000..4ef4110bc0742 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemResponseMock, getUpdateListItemOptionsMock } from '../mocks'; + +import { updateListItem } from './update_list_item'; +import { getListItem } from './get_list_item'; + +jest.mock('./get_list_item', () => ({ + getListItem: jest.fn(), +})); + +describe('update_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected with the id changed out for the elastic id when there is a list item to update', async () => { + const list = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list item to update', async () => { + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + expect(updatedList).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts new file mode 100644 index 0000000000000..ce4f8125d77af --- /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 0000000000000..f064543f1ec93 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getImportListItemsToStreamOptionsMock, + getListItemResponseMock, + getWriteBufferToItemsOptionsMock, +} from '../mocks'; + +import { + LinesResult, + importListItemsToStream, + writeBufferToItems, +} from './write_lines_to_bulk_list_items'; + +import { getListItemByValues } from '.'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('write_lines_to_bulk_list_items', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('importListItemsToStream', () => { + test('It imports a set of items to a write buffer by calling "getListItemByValues" with an empty buffer', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: [] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with a single value given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: ['127.0.0.1'] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with two values given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push('127.0.0.2\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith( + expect.objectContaining({ value: ['127.0.0.1', '127.0.0.2'] }) + ); + }); + }); + + describe('writeBufferToItems', () => { + test('It returns no duplicates and no lines processed when given empty items', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns no lines processed when given items but no buffer', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns 1 lines processed when given a buffer item that is not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing a second value as not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing two other values', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', '192.168.0.1', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 2, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters two duplicate values out and reports processes a single value', async () => { + const dataItem1 = getListItemResponseMock(); + dataItem1.value = '127.0.0.1'; + const dataItem2 = getListItemResponseMock(); + dataItem2.value = '127.0.0.2'; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([dataItem1, dataItem2]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [dataItem1.value, dataItem2.value, '192.168.0.0.1']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 2, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts new file mode 100644 index 0000000000000..1fe1023e28ab9 --- /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 0000000000000..63e9aeb61bad0 --- /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 0000000000000..0e0ae7b924e17 --- /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 0000000000000..32578fc739f26 --- /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 0000000000000..c3b6a484d8787 --- /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 0000000000000..d6ba435155c60 --- /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 0000000000000..dcf87b3ad1ef1 --- /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 0000000000000..f32273e3e7f76 --- /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 0000000000000..653a8da74a105 --- /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 0000000000000..1f9a33c191764 --- /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 0000000000000..216703f08f069 --- /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 0000000000000..22a738a340b25 --- /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 0000000000000..70b85fc97ebfa --- /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 0000000000000..e25eaaafd855e --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListTemplate } from './get_list_template'; + +jest.mock('./list_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_template.ts b/x-pack/plugins/lists/server/services/lists/get_list_template.ts new file mode 100644 index 0000000000000..9d93a051f2d10 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import listMappings from './list_mappings.json'; + +export const getListTemplate = (index: string): Record<string, unknown> => ({ + index_patterns: [`${index}-*`], + mappings: listMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, +}); diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts new file mode 100644 index 0000000000000..f704ef0b05b82 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_list'; +export * from './delete_list'; +export * from './get_list'; +export * from './get_list_template'; +export * from './update_list'; +export * from './get_list_index'; diff --git a/x-pack/plugins/lists/server/services/lists/list_mappings.json b/x-pack/plugins/lists/server/services/lists/list_mappings.json new file mode 100644 index 0000000000000..1136a53da787d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "name": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/list_policy.json b/x-pack/plugins/lists/server/services/lists/list_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts new file mode 100644 index 0000000000000..09bf0ee69c981 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListResponseMock, getUpdateListOptionsMock } from '../mocks'; + +import { updateList } from './update_list'; +import { getList } from './get_list'; + +jest.mock('./get_list', () => ({ + getList: jest.fn(), +})); + +describe('update_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected with the id changed out for the elastic id when there is a list to update', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list to update', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + expect(updatedList).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts new file mode 100644 index 0000000000000..55f110e9a8291 --- /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 0000000000000..0f4d92cabaa7a --- /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 0000000000000..960db293f1124 --- /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 0000000000000..1a005a76547f5 --- /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 0000000000000..6e4cc40efeed7 --- /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 0000000000000..58fd319589ea3 --- /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 0000000000000..1e7167547a6de --- /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 0000000000000..9d70dae969362 --- /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 0000000000000..4cc6d85cd947a --- /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 0000000000000..574e4afcb36f0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from './lists_services_mock_constants'; + +export const getIndexESListItemMock = (ip = VALUE): IndexEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip, + list_id: LIST_ID, + meta: META, + tie_breaker_id: TIE_BREAKER, + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts new file mode 100644 index 0000000000000..4e4d8d9c572e4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexEsListSchema } from '../../../common/schemas'; + +import { + DATE_NOW, + DESCRIPTION, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getIndexESListMock = (): IndexEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: META, + name: NAME, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts new file mode 100644 index 0000000000000..ab1bde48e7ebf --- /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 0000000000000..c15d417d10289 --- /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 0000000000000..1a30282ddaeba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, LIST_ITEM_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getListItemResponseMock = (): ListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + id: LIST_ITEM_ID, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts new file mode 100644 index 0000000000000..ea068d774c4ed --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, LIST_ID, NAME, USER } from './lists_services_mock_constants'; + +export const getListResponseMock = (): ListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + id: LIST_ID, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts new file mode 100644 index 0000000000000..5e9fd8995c0eb --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getSearchEsListItemMock = (): SearchEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip: VALUE, + keyword: undefined, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts new file mode 100644 index 0000000000000..6a565437617ba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchEsListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, NAME, USER } from './lists_services_mock_constants'; + +export const getSearchEsListMock = (): SearchEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts new file mode 100644 index 0000000000000..9f877c8168cca --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { SearchEsListItemSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_INDEX, LIST_ITEM_ID } from './lists_services_mock_constants'; +import { getSearchEsListItemMock } from './get_search_es_list_item_mock'; + +export const getSearchListItemMock = (): SearchResponse<SearchEsListItemSchema> => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts new file mode 100644 index 0000000000000..9728139eab42a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { SearchEsListSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_ID, LIST_INDEX } from './lists_services_mock_constants'; +import { getSearchEsListMock } from './get_search_es_list_mock'; + +export const getSearchListMock = (): SearchResponse<SearchEsListSchema> => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts new file mode 100644 index 0000000000000..4cc6577d5e531 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ShardsResponse } from 'elasticsearch'; + +export const getShardMock = (): ShardsResponse => ({ + failed: 0, + skipped: 0, + successful: 0, + total: 0, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts new file mode 100644 index 0000000000000..b60d6f5113e06 --- /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 0000000000000..e56ebc24bdae1 --- /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 0000000000000..9a77453b65d6a --- /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 0000000000000..96724c2a88045 --- /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 0000000000000..516264149fac7 --- /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 0000000000000..d174211f348ea --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const LIST_INDEX = '.lists'; +export const LIST_ITEM_INDEX = '.items'; +export const NAME = 'some name'; +export const DESCRIPTION = 'some description'; +export const LIST_ID = 'some-list-id'; +export const LIST_ITEM_ID = 'some-list-item-id'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const TIE_BREAKERS = [ + '21530991-4051-46ec-bc35-2afa09a1b0b5', + '3c662054-ae37-4aa9-9936-3e8e2ea26775', + '60e49a20-3a23-48b6-8bf9-ed5e3b70f7a0', + '38814080-a40f-4358-992a-3b875f9b7dec', + '29fa61be-aaaf-411c-a78a-7059e3f723f1', + '9c19c959-cb9d-4cd2-99e4-1ea2baf0ef0e', + 'd409308c-f94b-4b3a-8234-bbd7a80c9140', + '87824c99-cd83-45c4-8aa6-4ad95dfea62c', + '7b940c17-9355-479f-b882-f3e575718f79', + '5983ad0c-4ef4-4fa0-8308-80ab9ecc4f74', +]; +export const META = {}; +export const TYPE = 'ip'; +export const VALUE = '127.0.0.1'; +export const VALUE_2 = '255.255.255'; diff --git a/x-pack/plugins/lists/server/services/mocks/test_readable.ts b/x-pack/plugins/lists/server/services/mocks/test_readable.ts new file mode 100644 index 0000000000000..52ad6de484005 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/test_readable.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Readable } from 'stream'; + +export class TestReadable extends Readable { + public _read(): void {} +} diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts new file mode 100644 index 0000000000000..6e5dca7d54e5b --- /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 0000000000000..7a65e74bf4947 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +interface DeriveTypeFromItemOptions { + item: SearchEsListItemSchema; +} + +export const deriveTypeFromItem = ({ item }: DeriveTypeFromItemOptions): Type => { + if (item.ip != null) { + return 'ip'; + } else if (item.keyword != null) { + return 'keyword'; + } else { + throw new ErrorWithStatusCode( + `Was expecting a valid type from the Elastic Search List Item such as ip or keyword but did not found one here ${JSON.stringify( + item + )}`, + 400 + ); + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts new file mode 100644 index 0000000000000..3f50efe0c6c56 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Type } from '../../../common/schemas'; + +export type QueryFilterType = Array< + { term: { list_id: string } } | { terms: { ip: string[] } } | { terms: { keyword: string[] } } +>; + +export const getQueryFilterFromTypeValue = ({ + type, + value, + listId, +}: { + type: Type; + value: string[]; + listId: string; + // We disable the consistent return since we want to use typescript for exhaustive type checks + // eslint-disable-next-line consistent-return +}): QueryFilterType => { + const filter: QueryFilterType = [{ term: { list_id: listId } }]; + switch (type) { + case 'ip': { + return [...filter, ...[{ terms: { ip: value } }]]; + } + case 'keyword': { + return [...filter, ...[{ terms: { keyword: value } }]]; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_space.ts b/x-pack/plugins/lists/server/services/utils/get_space.ts new file mode 100644 index 0000000000000..e23f963b2c40d --- /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 0000000000000..1ddad047da722 --- /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 0000000000000..f256b6cb8f2d5 --- /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 0000000000000..9cf673081d320 --- /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 0000000000000..e68851dc3582b --- /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 0000000000000..e78debc8e4349 --- /dev/null +++ b/x-pack/plugins/lists/server/siem_server_deps.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + transformError, + buildSiemResponse, +} from '../../siem/server/lib/detection_engine/routes/utils'; +export { deleteTemplate } from '../../siem/server/lib/detection_engine/index/delete_template'; +export { deletePolicy } from '../../siem/server/lib/detection_engine/index/delete_policy'; +export { deleteAllIndex } from '../../siem/server/lib/detection_engine/index/delete_all_index'; +export { setPolicy } from '../../siem/server/lib/detection_engine/index/set_policy'; +export { setTemplate } from '../../siem/server/lib/detection_engine/index/set_template'; +export { getTemplateExists } from '../../siem/server/lib/detection_engine/index/get_template_exists'; +export { getPolicyExists } from '../../siem/server/lib/detection_engine/index/get_policy_exists'; +export { createBootstrapIndex } from '../../siem/server/lib/detection_engine/index/create_bootstrap_index'; +export { getIndexExists } from '../../siem/server/lib/detection_engine/index/get_index_exists'; +export { buildRouteValidation } from '../../siem/server/utils/build_validation/route_validation'; +export { validate } from '../../siem/server/lib/detection_engine/routes/rules/validate'; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts new file mode 100644 index 0000000000000..7d509c4e27167 --- /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 c8db284a5c4f1..38c56405787eb 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/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 af836ceffa4b7..0000000000000 --- 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 d79eda16037cb..0000000000000 --- 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 c3a8f59c4c736..0000000000000 --- 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/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 27ea52bfed044..f1cb1a8864753 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 ca3c6d325687d..b0d4d9d357988 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 0000000000000..b8c652909408a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TOCEntryActionsPopover is rendered 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "editLayerButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="pencil" + />, + "name": "Edit layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": <EuiIcon + size="m" + type="copy" + />, + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": <EuiIcon + size="m" + type="trash" + />, + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; + +exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": true, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": "Layer does not support fit to data", + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "editLayerButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="pencil" + />, + "name": "Edit layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": <EuiIcon + size="m" + type="copy" + />, + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": <EuiIcon + size="m" + type="trash" + />, + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; + +exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts new file mode 100644 index 0000000000000..1437370557efc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../../../../../reducers/store'; +import { + fitToLayerExtent, + toggleLayerVisible, + cloneLayer, + removeLayer, +} from '../../../../../../actions/map_actions'; +import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors'; +import { getIsReadOnly } from '../../../../../../selectors/ui_selectors'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; + +function mapStateToProps(state: MapStoreState) { + return { + isReadOnly: getIsReadOnly(state), + isUsingSearch: isUsingSearch(state), + zoom: getMapZoom(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch<AnyAction>) { + return { + cloneLayer: (layerId: string) => { + dispatch(cloneLayer(layerId)); + }, + fitToBounds: (layerId: string) => { + dispatch(fitToLayerExtent(layerId)); + }, + removeLayer: (layerId: string) => { + dispatch(removeLayer(layerId)); + }, + toggleVisible: (layerId: string) => { + dispatch(toggleLayerVisible(layerId)); + }, + }; +} + +const connectedTOCEntryActionsPopover = connect( + mapStateToProps, + mapDispatchToProps +)(TOCEntryActionsPopover); +export { connectedTOCEntryActionsPopover as TOCEntryActionsPopover }; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx new file mode 100644 index 0000000000000..b873119fd7d13 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { AbstractLayer, ILayer } from '../../../../../../layers/layer'; +import { AbstractSource, ISource } from '../../../../../../layers/sources/source'; +import { AbstractStyle, IStyle } from '../../../../../../layers/styles/style'; + +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; + +let supportsFitToBounds: boolean; + +class MockSource extends AbstractSource implements ISource {} + +class MockStyle extends AbstractStyle implements IStyle {} + +class LayerMock extends AbstractLayer implements ILayer { + constructor() { + const sourceDescriptor = { + type: 'mySourceType', + }; + const source = new MockSource(sourceDescriptor); + const style = new MockStyle({ type: 'myStyleType' }); + const layerDescriptor = { + id: 'testLayer', + sourceDescriptor, + }; + super({ layerDescriptor, source, style }); + } + + async supportsFitToBounds(): Promise<boolean> { + return supportsFitToBounds; + } + + isVisible() { + return true; + } + + getIconAndTooltipContent(zoom: number, isUsingSearch: boolean) { + return { + icon: <span>mockIcon</span>, + tooltipContent: `simulated tooltip content at zoom: ${zoom}`, + footnotes: [ + { + icon: <span>mockFootnoteIcon</span>, + message: `simulated footnote at isUsingSearch: ${isUsingSearch}`, + }, + ], + }; + } +} + +const defaultProps = { + cloneLayer: () => {}, + displayName: 'layer 1', + editLayer: () => {}, + escapedDisplayName: 'layer1', + fitToBounds: () => {}, + isEditButtonDisabled: false, + isReadOnly: false, + isUsingSearch: true, + layer: new LayerMock(), + removeLayer: () => {}, + toggleVisible: () => {}, + zoom: 0, +}; + +describe('TOCEntryActionsPopover', () => { + beforeEach(() => { + supportsFitToBounds = true; + }); + + test('is rendered', async () => { + const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should not show edit actions in read only mode', async () => { + const component = shallowWithIntl( + <TOCEntryActionsPopover {...defaultProps} isReadOnly={true} /> + ); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should disable fit to data when supportsFitToBounds is false', async () => { + supportsFitToBounds = false; + const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx new file mode 100644 index 0000000000000..d628cca61de11 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; + +import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ILayer } from '../../../../../../layers/layer'; + +interface Props { + cloneLayer: (layerId: string) => void; + displayName: string; + editLayer: () => void; + escapedDisplayName: string; + fitToBounds: (layerId: string) => void; + isEditButtonDisabled: boolean; + isReadOnly: boolean; + isUsingSearch: boolean; + layer: ILayer; + removeLayer: (layerId: string) => void; + toggleVisible: (layerId: string) => void; + zoom: number; +} + +interface State { + isPopoverOpen: boolean; + supportsFitToBounds: boolean; +} + +export class TOCEntryActionsPopover extends Component<Props, State> { + private _isMounted: boolean = false; + + state = { + isPopoverOpen: false, + supportsFitToBounds: false, + }; + + componentDidMount() { + this._isMounted = true; + this._loadSupportsFitToBounds(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadSupportsFitToBounds() { + const supportsFitToBounds = await this.props.layer.supportsFitToBounds(); + if (this._isMounted) { + this.setState({ supportsFitToBounds }); + } + } + + _togglePopover = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState(() => ({ + isPopoverOpen: false, + })); + }; + + _cloneLayer() { + this.props.cloneLayer(this.props.layer.getId()); + } + + _fitToBounds() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _removeLayer() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _toggleVisible() { + this.props.toggleVisible(this.props.layer.getId()); + } + + _renderPopoverToggleButton() { + const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent( + this.props.zoom, + this.props.isUsingSearch + ); + + const footnoteIcons = footnotes.map((footnote, index) => { + return ( + <Fragment key={index}> + {''} + {footnote.icon} + </Fragment> + ); + }); + const footnoteTooltipContent = footnotes.map((footnote, index) => { + return ( + <div key={index}> + {footnote.icon} {footnote.message} + </div> + ); + }); + + return ( + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + position="top" + title={this.props.displayName} + content={ + <Fragment> + {tooltipContent} + {footnoteTooltipContent} + </Fragment> + } + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + size="xs" + flush="left" + color="text" + onClick={this._togglePopover} + data-test-subj={`layerTocActionsPanelToggleButton${this.props.escapedDisplayName}`} + > + <span className="mapTocEntry__layerNameIcon">{icon}</span> + {this.props.displayName} {footnoteIcons} + </EuiButtonEmpty> + </EuiToolTip> + ); + } + + _getActionsPanel() { + const actionItems = [ + { + name: i18n.translate('xpack.maps.layerTocActions.fitToDataTitle', { + defaultMessage: 'Fit to data', + }), + icon: <EuiIcon type="search" size="m" />, + 'data-test-subj': 'fitToBoundsButton', + toolTipContent: this.state.supportsFitToBounds + ? null + : i18n.translate('xpack.maps.layerTocActions.noFitSupportTooltip', { + defaultMessage: 'Layer does not support fit to data', + }), + disabled: !this.state.supportsFitToBounds, + onClick: () => { + this._closePopover(); + this._fitToBounds(); + }, + }, + { + name: this.props.layer.isVisible() + ? i18n.translate('xpack.maps.layerTocActions.hideLayerTitle', { + defaultMessage: 'Hide layer', + }) + : i18n.translate('xpack.maps.layerTocActions.showLayerTitle', { + defaultMessage: 'Show layer', + }), + icon: <EuiIcon type={this.props.layer.isVisible() ? 'eye' : 'eyeClosed'} size="m" />, + 'data-test-subj': 'layerVisibilityToggleButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this._toggleVisible(); + }, + }, + ]; + + if (!this.props.isReadOnly) { + actionItems.push({ + disabled: this.props.isEditButtonDisabled, + name: i18n.translate('xpack.maps.layerTocActions.editLayerTitle', { + defaultMessage: 'Edit layer', + }), + icon: <EuiIcon type="pencil" size="m" />, + 'data-test-subj': 'editLayerButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this.props.editLayer(); + }, + }); + actionItems.push({ + name: i18n.translate('xpack.maps.layerTocActions.cloneLayerTitle', { + defaultMessage: 'Clone layer', + }), + icon: <EuiIcon type="copy" size="m" />, + toolTipContent: null, + 'data-test-subj': 'cloneLayerButton', + onClick: () => { + this._closePopover(); + this._cloneLayer(); + }, + }); + actionItems.push({ + name: i18n.translate('xpack.maps.layerTocActions.removeLayerTitle', { + defaultMessage: 'Remove layer', + }), + icon: <EuiIcon type="trash" size="m" />, + toolTipContent: null, + 'data-test-subj': 'removeLayerButton', + onClick: () => { + this._closePopover(); + this._removeLayer(); + }, + }); + } + + return { + id: 0, + title: i18n.translate('xpack.maps.layerTocActions.layerActionsTitle', { + defaultMessage: 'Layer actions', + }), + items: actionItems, + }; + } + + render() { + return ( + <EuiPopover + id="contextMenu" + className="mapLayTocActions" + button={this._renderPopoverToggleButton()} + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + panelPaddingSize="none" + withTitle + anchorPosition="leftUp" + anchorClassName="mapLayTocActions__popoverAnchor" + > + <EuiContextMenu + initialPanelId={0} + panels={[this._getActionsPanel()]} + data-test-subj={`layerTocActionsPanel${this.props.escapedDisplayName}`} + /> + </EuiPopover> + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index fe56523fb2580..c0ce24fef9cd8 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/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx index 13fe447cec3da..dccf413b489f1 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; @@ -87,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; }; @@ -212,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()} />, }; 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 633e8c86d8c94..7715541b1c52d 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -20,6 +20,7 @@ export type RenderWizardArguments = { }; export type LayerWizard = { + checkVisibility?: () => boolean; description: string; icon: string; isIndexingSource?: boolean; @@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) { } export function getLayerWizards(): LayerWizard[] { - return [...registry]; + return registry.filter(layerWizard => { + return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true; + }); } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index f31e770df2d95..a6e2e7f42657c 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry' import { EMSFileCreateSourceEditor } from './create_source_editor'; // @ts-ignore import { EMSFileSource, sourceTitle } from './ems_file_source'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBoundariesLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index ced33a0bcf84a..fc745edbabee8 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBaseMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js index ccd8bc4a859db..87733e347aa2a 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js @@ -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/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 4321501760faf..a9adec2bda2c8 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { getKibanaRegionList } from '../../../meta'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const regions = getKibanaRegionList(); + return regions.length; + }, description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index aeea2d6084f84..141fabeedd3e5 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../tile_layer'; +// @ts-ignore +import { getKibanaTileMap } from '../../../meta'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const tilemap = getKibanaTileMap(); + return !!tilemap.url; + }, description: i18n.translate('xpack.maps.source.kbnTMSDescription', { defaultMessage: 'Tile map service configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js index 2a4843c78635f..bf1f9de6b2d2b 100644 --- a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -8,101 +8,74 @@ import { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; import { DataRequest } from './data_request'; describe('updateDueToExtent', () => { - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { - return false; - }, + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, }; - expect(updateDueToExtent(sourceMock)).toBe(false); + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); }); - describe('source is extent aware', () => { - const sourceMock = { - isFilterByMapBounds: () => { - return true; - }, + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); - it('should be false when buffers are the same', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - false - ); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - false - ); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - expect( - updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer } - ) - ).toBe(true); - }); + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect( + updateDueToExtent({ buffer: oldBuffer, areResultsTrimmed: true }, { buffer: newBuffer }) + ).toBe(true); + }); - it('should be true when meta has no old buffer', async () => { - expect(updateDueToExtent(sourceMock)).toBe(true); - }); + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent()).toBe(true); + }); - it('should be true when the new buffer is not contained in the old buffer', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - true - ); - }); + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); }); }); diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts index 758cc35f41fbb..8398bd7af39ad 100644 --- a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts @@ -15,16 +15,7 @@ import { DataRequest } from './data_request'; const SOURCE_UPDATE_REQUIRED = true; const NO_SOURCE_UPDATE_REQUIRED = false; -export function updateDueToExtent( - source: ISource, - prevMeta: DataMeta = {}, - nextMeta: DataMeta = {} -) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - +export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = {}) { const { buffer: previousBuffer } = prevMeta; const { buffer: newBuffer } = nextMeta; @@ -134,7 +125,10 @@ export async function canSkipSourceUpdate({ updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); } - const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + let updateDueToExtentChange = false; + if (extentAware) { + updateDueToExtentChange = updateDueToExtent(prevMeta, nextMeta); + } const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index 17b7f8152d76d..582e34bce2e98 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -175,8 +175,7 @@ export class VectorLayer extends AbstractLayer { } async getBounds(dataFilters) { - const isStaticLayer = - !this.getSource().isBoundsAware() || !this.getSource().isFilterByMapBounds(); + const isStaticLayer = !this.getSource().isBoundsAware(); if (isStaticLayer) { return this._getBoundsBasedOnData(); } diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b..c3245e8e98db2 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -36,12 +36,15 @@ function fetchFunction(...args) { return fetch(...args); } +export function isEmsEnabled() { + return getInjectedVarFunc()('isEmsEnabled', true); +} + let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { + if (isEmsEnabled()) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -86,7 +89,7 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!getInjectedVarFunc()('isEmsEnabled', true)) { + if (!isEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index 4d0f652af982a..bc881d06f62ce 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 0000000000000..2df2ecd22e078 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/settings.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const FILE_DATA_VISUALIZER_MAX_FILE_SIZE = 'ml:fileDataVisualizerMaxFileSize'; diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 2a449c95faa5b..da5fd3ac25209 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 f2ddadccb2170..0000000000000 --- a/x-pack/plugins/ml/common/types/ml_config.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import { MAX_FILE_SIZE } from '../constants/file_datavisualizer'; - -export const configSchema = schema.object({ - file_data_visualizer: schema.object({ - max_file_size: schema.string({ defaultValue: MAX_FILE_SIZE }), - }), -}); - -export type MlConfigType = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/ml/common/util/job_utils.d.ts b/x-pack/plugins/ml/common/util/job_utils.d.ts index bfad422e0ab48..4528fbfbb774d 100644 --- a/x-pack/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/plugins/ml/common/util/job_utils.d.ts @@ -52,3 +52,5 @@ export function getLatestDataOrBucketTimestamp( ): number; export function prefixDatafeedId(datafeedId: string, prefix: string): string; + +export function splitIndexPatternNames(indexPatternName: string): string[]; diff --git a/x-pack/plugins/ml/common/util/job_utils.js b/x-pack/plugins/ml/common/util/job_utils.js index de0aa4b886629..8fe5733ce67bd 100644 --- a/x-pack/plugins/ml/common/util/job_utils.js +++ b/x-pack/plugins/ml/common/util/job_utils.js @@ -588,3 +588,9 @@ export function processCreatedBy(customSettings) { delete customSettings.created_by; } } + +export function splitIndexPatternNames(indexPatternName) { + return indexPatternName.includes(',') + ? indexPatternName.split(',').map(i => i.trim()) + : [indexPatternName]; +} diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index f1facd18b9da5..e9796fcbb0fe4 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -15,14 +15,10 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; -import { MlConfigType } from '../../common/types/ml_config'; import { MlRouter } from './routing'; -type MlDependencies = MlSetupDependencies & - MlStartDependencies & { - mlConfig: MlConfigType; - }; +type MlDependencies = MlSetupDependencies & MlStartDependencies; interface AppProps { coreStart: CoreStart; @@ -78,7 +74,6 @@ export const renderApp = ( http: coreStart.http, security: deps.security, urlGenerators: deps.share.urlGenerators, - mlConfig: deps.mlConfig, }); const mlLicense = setLicenseCache(deps.licensing); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 7b6464570e55c..7d966949624c1 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -9,11 +9,13 @@ import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; import { + MAX_FILE_SIZE, MAX_FILE_SIZE_BYTES, ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, } from '../../../../../../common/constants/file_datavisualizer'; -import { getMlConfig } from '../../../../util/dependency_cache'; +import { getUiSettings } from '../../../../util/dependency_cache'; +import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../../../../../common/constants/settings'; const DEFAULT_LINES_TO_SAMPLE = 1000; const UPLOAD_SIZE_MB = 5; @@ -62,13 +64,13 @@ export function readFile(file: File) { } export function getMaxBytes() { - const maxFileSize = getMlConfig().file_data_visualizer.max_file_size; + const maxFileSize = getUiSettings().get(FILE_DATA_VISUALIZER_MAX_FILE_SIZE, MAX_FILE_SIZE); // @ts-ignore const maxBytes = numeral(maxFileSize.toUpperCase()).value(); if (maxBytes < MAX_FILE_SIZE_BYTES) { return MAX_FILE_SIZE_BYTES; } - return maxBytes < ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; + return maxBytes <= ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; } export function getMaxBytesFormatted() { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 306fd82dc8758..9dda8eec206e4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -7,6 +7,7 @@ import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; +import { splitIndexPatternNames } from '../../../../../../../common/util/job_utils'; export function createEmptyJob(): Job { return { @@ -28,7 +29,7 @@ export function createEmptyDatafeed(indexPatternTitle: IndexPatternTitle): Dataf return { datafeed_id: '', job_id: '', - indices: [indexPatternTitle], + indices: splitIndexPatternNames(indexPatternTitle), query: {}, }; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 50a84eb3d11cb..69df2773f9f8d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -50,7 +50,7 @@ function getWizardUrlFromCloningJob(job: CombinedJob) { break; } - const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices[0]); + const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices.join()); return `jobs/new_job/${page}?index=${indexPatternId}&_g=()`; } diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index b2eda12abc578..c379cd702daee 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -82,14 +82,14 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => { title={ <h2> {i18n.translate('xpack.ml.overview.analyticsList.createFirstJobMessage', { - defaultMessage: 'Create your first analytics job', + defaultMessage: 'Create your first data frame analytics job', })} </h2> } body={ <p> {i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', { - defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`, + defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotates it with the results. The job puts the annotated data and a copy of the source data in a new index.`, })} </p> } diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 5f5c3f7c28670..dac39b1a2071d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -172,7 +172,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => { <Fragment> <p> {i18n.translate('xpack.ml.overview.anomalyDetection.emptyPromptText', { - defaultMessage: `Machine learning makes it easy to detect anomalies in time series data stored in Elasticsearch. Track one metric from a single machine or hundreds of metrics across thousands of machines. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, + defaultMessage: `Anomaly detection enables you to find unusual behavior in time series data. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, })} </p> </Fragment> diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 219c195bab111..3e4e9cfbd2b66 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -11,7 +11,6 @@ import { useMlKibana } from '../../contexts/kibana'; const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; -const whatIsMachineLearningLink = 'https://www.elastic.co/what-is/elasticsearch-machine-learning'; interface Props { createAnomalyDetectionJobDisabled: boolean; @@ -60,7 +59,7 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } <p> <FormattedMessage id="xpack.ml.overview.gettingStartedSectionText" - defaultMessage="Welcome to Machine Learning. Get started by reviewing our {docs} or {createJob}. For more information about machine learning in the Elastic stack please see {whatIsMachineLearning}. We recommend using {transforms} to create feature indices for analytics jobs." + defaultMessage="Welcome to Machine Learning. Get started by reviewing our {docs} or {createJob}. We recommend using {transforms} to create feature indices for analytics jobs." values={{ docs: ( <EuiLink href={docsLink} target="blank"> @@ -79,14 +78,6 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } /> </EuiLink> ), - whatIsMachineLearning: ( - <EuiLink href={whatIsMachineLearningLink} target="blank"> - <FormattedMessage - id="xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning" - defaultMessage="here" - /> - </EuiLink> - ), }} /> </p> @@ -96,7 +87,7 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } <p> <FormattedMessage id="xpack.ml.overview.feedbackSectionText" - defaultMessage="If you have input or suggestions regarding your experience with Machine Learning please feel free to submit {feedbackLink}." + defaultMessage="If you have input or suggestions regarding your experience, please submit {feedbackLink}." values={{ feedbackLink: ( <EuiLink href={feedbackLink} target="blank"> diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 3fd228377c57e..5a062320ca6c5 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -107,7 +107,7 @@ export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, rad } // this replicates Kibana's filterAxisLabels() behavior -// which can be found in src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js +// which can be found in src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js // axis labels which overflow the chart's boundaries will be removed export function filterAxisLabels(selection, chartWidth) { if (selection === undefined || selection.selectAll === undefined) { diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index f8dd2a37dd589..356da38d5ad08 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -23,7 +23,6 @@ import { } from 'kibana/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { SecurityPluginSetup } from '../../../../security/public'; -import { MlConfigType } from '../../../common/types/ml_config'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -43,7 +42,6 @@ export interface DependencyCache { security: SecurityPluginSetup | undefined | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; - mlConfig: MlConfigType | null; } const cache: DependencyCache = { @@ -64,7 +62,6 @@ const cache: DependencyCache = { security: null, i18n: null, urlGenerators: null, - mlConfig: null, }; export function setDependencyCache(deps: Partial<DependencyCache>) { @@ -85,7 +82,6 @@ export function setDependencyCache(deps: Partial<DependencyCache>) { cache.security = deps.security || null; cache.i18n = deps.i18n || null; cache.urlGenerators = deps.urlGenerators || null; - cache.mlConfig = deps.mlConfig || null; } export function getTimefilter() { @@ -206,13 +202,6 @@ export function getGetUrlGenerator() { return cache.urlGenerators.getUrlGenerator; } -export function getMlConfig() { - if (cache.mlConfig === null) { - throw new Error("mlConfig hasn't been initialized"); - } - return cache.mlConfig; -} - export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 4697496270edf..8070f94a1264d 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { PluginInitializer } from 'kibana/public'; import './index.scss'; import { MlPlugin, @@ -19,6 +19,6 @@ export const plugin: PluginInitializer< MlPluginStart, MlSetupDependencies, MlStartDependencies -> = (context: PluginInitializerContext) => new MlPlugin(context); +> = () => new MlPlugin(); export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index e3b8441db432e..d37d1fd815eb5 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -5,13 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - Plugin, - CoreStart, - CoreSetup, - AppMountParameters, - PluginInitializerContext, -} from 'kibana/public'; +import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -25,7 +19,6 @@ 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; @@ -41,10 +34,7 @@ export interface MlSetupDependencies { } export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { - constructor(private readonly initializerContext: PluginInitializerContext) {} - setup(core: CoreSetup<MlStartDependencies, MlPluginStart>, pluginsSetup: MlSetupDependencies) { - const mlConfig = this.initializerContext.config.get<MlConfigType>(); core.application.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.plugin.title', { @@ -67,7 +57,6 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { usageCollection: pluginsSetup.usageCollection, licenseManagement: pluginsSetup.licenseManagement, home: pluginsSetup.home, - mlConfig, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/config.ts b/x-pack/plugins/ml/server/config.ts deleted file mode 100644 index 7cef6f17bbefb..0000000000000 --- a/x-pack/plugins/ml/server/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginConfigDescriptor } from 'kibana/server'; -import { MlConfigType, configSchema } from '../common/types/ml_config'; - -export const config: PluginConfigDescriptor<MlConfigType> = { - exposeToBrowser: { - file_data_visualizer: true, - }, - schema: configSchema, -}; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 6e638d647a387..175c20bf49c94 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -9,5 +9,3 @@ import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); - -export { config } from './config'; diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 5093801d2d184..b6e95ae8373ee 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 a2ad83c5522de..d955cf981faca 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 0000000000000..38b1f5e3fc083 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/register_settings.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../common/constants/settings'; +import { MAX_FILE_SIZE } from '../../common/constants/file_datavisualizer'; + +export function registerKibanaSettings(coreSetup: CoreSetup) { + coreSetup.uiSettings.register({ + [FILE_DATA_VISUALIZER_MAX_FILE_SIZE]: { + name: i18n.translate('xpack.ml.maxFileSizeSettingsName', { + defaultMessage: 'File Data Visualizer maximum file upload size', + }), + value: MAX_FILE_SIZE, + description: i18n.translate('xpack.ml.maxFileSizeSettingsDescription', { + defaultMessage: + 'Sets the file size limit when importing data in the File Data Visualizer. The highest supported value for this setting is 1GB.', + }), + category: ['Machine Learning'], + schema: schema.string(), + validation: { + regexString: '\\d+[mMgG][bB]', + message: i18n.translate('xpack.ml.maxFileSizeSettingsError', { + defaultMessage: 'Should be a valid data size. e.g. 200MB, 1GB', + }), + }, + }, + }); +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 84f81a30f36b8..40b2a524151b3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -29,7 +29,11 @@ import { JobSpecificOverride, isGeneralJobOverride, } from '../../../common/types/modules'; -import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; +import { + getLatestDataOrBucketTimestamp, + prefixDatafeedId, + splitIndexPatternNames, +} from '../../../common/util/job_utils'; import { mlLog } from '../../client/log'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; import { fieldsServiceProvider } from '../fields_service'; @@ -828,9 +832,7 @@ export class DataRecognizer { updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed - const indexPatternNames = this.indexPatternName.includes(',') - ? this.indexPatternName.split(',').map(i => i.trim()) - : [this.indexPatternName]; + const indexPatternNames = splitIndexPatternNames(this.indexPatternName); moduleConfig.datafeeds.forEach(df => { const newIndices: string[] = []; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 67e80a3bc44c0..969b74194148b 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 d5abebda00caa..e6d082cd74aab 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -54,6 +54,9 @@ export function annotationRoutes( validate: { body: getAnnotationsSchema, }, + options: { + tags: ['access:ml:canGetAnnotations'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -86,6 +89,9 @@ export function annotationRoutes( validate: { body: indexAnnotationSchema, }, + options: { + tags: ['access:ml:canCreateAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -130,6 +136,9 @@ export function annotationRoutes( validate: { params: deleteAnnotationSchema, }, + options: { + tags: ['access:ml:canDeleteAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index ca63d69f403f6..63cd5498231af 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/_stats', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -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 a17601f74ae93..9c80651a13999 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/calendars', validate: false, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdsSchema, }, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; @@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { params: calendarIdSchema, body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdSchema, }, + options: { + tags: ['access:ml:canDeleteCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index dd9e0ea66aa9d..32cb2b343f876 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics/_stats', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, body: dataAnalyticsJobConfigSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsEvaluateSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsExplainSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canDeleteDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, query: stopsDataFrameAnalyticsJobQuerySchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 20029fbd8d1a6..04008a896a1a2 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 ec667e1d305f5..1fa1d408372da 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds/_stats', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canCreateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canUpdateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, query: deleteDatafeedQuerySchema, }, + options: { + tags: ['access:ml:canDeleteDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: startDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 577e8e0161342..b0f13df294145 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 3f3fc3f547b6a..0f389f9505943 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['text/*', 'application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { @@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 738c25070358d..d5287c349a8fc 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: createFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { params: filterIdSchema, body: updateFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canDeleteFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/_stats', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e434936beba63..fb3ef7fc41c76 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: indicesSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1fe5a7af95d4f..5acc89e7d13be 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 149ca2591fd76..05c44e1da9757 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 dd2bd9deadf43..632166d6d5fb8 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: estimateBucketSpanSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: modelMemoryLimitSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateCardinalitySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateJobSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2891144fc4574..622ae66ede426 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 59458b1e486db..e4a9abb0784be 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/notification_settings', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 89c267340fe52..94ca0827ccfa5 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: anomaliesTableDataSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryDefinitionSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: maxAnomalyScoreSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryExamplesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: partitionFieldValuesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d5fe45728c56c..7ae7dd8eef065 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 d4cd61a7fa4f7..678e81d3526ac 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 edd6142455dfb..eeed7b4d5acf6 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -245,7 +245,7 @@ export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_ST /** * Matches the id for the built-in in email action type - * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; diff --git a/x-pack/plugins/remote_clusters/public/application/_hacks.scss b/x-pack/plugins/remote_clusters/public/application/_hacks.scss new file mode 100644 index 0000000000000..b7d81885e716d --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/_hacks.scss @@ -0,0 +1,25 @@ +// Remote clusters plugin hacks + +// Prefix all styles with "remoteClusters" to avoid conflicts. +// Examples +// remoteClustersChart +// remoteClustersChart__legend +// remoteClustersChart__legend--small +// remoteClustersChart__legend-isLoading + +/** + * 1. Override EuiFormRow styles. Otherwise the switch will jump around when toggled on and off, + * as the 'Reset to defaults' link is added to and removed from the DOM. + * 2. Fix the positioning. + */ +.remoteClusterSkipIfUnavailableSwitch { + justify-content: flex-start !important; /* 1 */ + padding-top: $euiSizeS !important; +} + +/** + * 1. Prevent inherited flexbox layout from compressing this element on IE. + */ + .remoteClustersConnectionStatus__message { + flex-basis: auto !important; /* 1 */ +} diff --git a/x-pack/plugins/remote_clusters/public/application/index.js b/x-pack/plugins/remote_clusters/public/application/index.js index f2d788c741342..cf6e855ba58df 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.js +++ b/x-pack/plugins/remote_clusters/public/application/index.js @@ -13,6 +13,8 @@ import { App } from './app'; import { remoteClustersStore } from './store'; import { AppContextProvider } from './app_context'; +import './_hacks.scss'; + export const renderApp = (elem, I18nContext, appDependencies) => { render( <I18nContext> diff --git a/x-pack/plugins/security/common/licensing/license_features.ts b/x-pack/plugins/security/common/licensing/license_features.ts index 5184ab0e962bd..571d2630b2b17 100644 --- a/x-pack/plugins/security/common/licensing/license_features.ts +++ b/x-pack/plugins/security/common/licensing/license_features.ts @@ -33,6 +33,11 @@ export interface SecurityLicenseFeatures { */ readonly showRoleMappingsManagement: boolean; + /** + * Indicates whether we allow users to access agreement UI and acknowledge it. + */ + readonly allowAccessAgreement: boolean; + /** * Indicates whether we allow users to define document level security in roles. */ diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index 5bdfa7d4886aa..9dec665614635 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -18,6 +18,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-es-unavailable', @@ -37,6 +38,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-xpack-unavailable', @@ -60,6 +62,7 @@ describe('license features', function() { expect(subscriptionHandler.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "allowAccessAgreement": false, "allowLogin": false, "allowRbac": false, "allowRoleDocumentLevelSecurity": false, @@ -78,6 +81,7 @@ describe('license features', function() { expect(subscriptionHandler.mock.calls[1]).toMatchInlineSnapshot(` Array [ Object { + "allowAccessAgreement": true, "allowLogin": true, "allowRbac": true, "allowRoleDocumentLevelSecurity": true, @@ -94,7 +98,7 @@ describe('license features', function() { } }); - it('should show login page and other security elements, allow RBAC but forbid role mappings, DLS, and sub-feature privileges if license is basic.', () => { + it('should show login page and other security elements, allow RBAC but forbid paid features if license is basic.', () => { const mockRawLicense = licensingMock.createLicense({ features: { security: { isEnabled: true, isAvailable: true } }, }); @@ -109,6 +113,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: true, @@ -131,6 +136,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -138,7 +144,7 @@ describe('license features', function() { }); }); - it('should allow role mappings and sub-feature privileges, but not DLS/FLS if license = gold', () => { + it('should allow role mappings, access agreement and sub-feature privileges, but not DLS/FLS if license = gold', () => { const mockRawLicense = licensingMock.createLicense({ license: { mode: 'gold', type: 'gold' }, features: { security: { isEnabled: true, isAvailable: true } }, @@ -152,6 +158,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: true, + allowAccessAgreement: true, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: true, @@ -159,7 +166,7 @@ describe('license features', function() { }); }); - it('should allow to login, allow RBAC, role mappings, sub-feature privileges, and DLS if license >= platinum', () => { + it('should allow to login, allow RBAC, role mappings, access agreement, sub-feature privileges, and DLS if license >= platinum', () => { const mockRawLicense = licensingMock.createLicense({ license: { mode: 'platinum', type: 'platinum' }, features: { security: { isEnabled: true, isAvailable: true } }, @@ -173,6 +180,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: true, + allowAccessAgreement: true, allowRoleDocumentLevelSecurity: true, allowRoleFieldLevelSecurity: true, allowRbac: true, diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 34bc44b88e40d..7815798d6a9f3 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -71,6 +71,7 @@ export class SecurityLicenseService { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -88,6 +89,7 @@ export class SecurityLicenseService { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -102,6 +104,7 @@ export class SecurityLicenseService { allowLogin: true, showLinks: true, showRoleMappingsManagement: isLicenseGoldOrBetter, + allowAccessAgreement: isLicenseGoldOrBetter, allowSubFeaturePrivileges: isLicenseGoldOrBetter, // Only platinum and trial licenses are compliant with field- and document-level security. allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter, diff --git a/x-pack/plugins/security/common/login_state.ts b/x-pack/plugins/security/common/login_state.ts index 4342e82d2f90b..fd2b1cb8d1cf7 100644 --- a/x-pack/plugins/security/common/login_state.ts +++ b/x-pack/plugins/security/common/login_state.ts @@ -6,15 +6,24 @@ import { LoginLayout } from './licensing'; +export interface LoginSelectorProvider { + type: string; + name: string; + usesLoginForm: boolean; + description?: string; + hint?: string; + icon?: string; +} + export interface LoginSelector { enabled: boolean; - providers: Array<{ type: string; name: string; description?: string }>; + providers: LoginSelectorProvider[]; } export interface LoginState { layout: LoginLayout; allowLogin: boolean; - showLoginForm: boolean; requiresSecureConnection: boolean; + loginHelp?: string; selector: LoginSelector; } diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts new file mode 100644 index 0000000000000..c668c6ccf71d1 --- /dev/null +++ b/x-pack/plugins/security/common/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Type and name tuple to identify provider used to authenticate user. + */ +export interface AuthenticationProvider { + type: string; + name: string; +} + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: AuthenticationProvider; +} diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts index cd3ef34858b19..41567a04fe030 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; import { AuthenticationServiceSetup } from '../authentication'; -import { UserAPIClient } from '../management'; interface CreateDeps { application: ApplicationSetup; @@ -28,9 +27,14 @@ export const accountManagementApp = Object.freeze({ navLinkStatus: 3, appRoute: '/security/account', async mount({ element }: AppMountParameters) { - const [[coreStart], { renderAccountManagementPage }] = await Promise.all([ + const [ + [coreStart], + { renderAccountManagementPage }, + { UserAPIClient }, + ] = await Promise.all([ getStartServices(), import('./account_management_page'), + import('../management'), ]); coreStart.chrome.setBreadcrumbs([{ text: title }]); diff --git a/x-pack/plugins/security/public/authentication/_index.scss b/x-pack/plugins/security/public/authentication/_index.scss deleted file mode 100644 index 0a423c00f0218..0000000000000 --- a/x-pack/plugins/security/public/authentication/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Component styles -@import './components/index'; - -// Login styles -@import './login/index'; diff --git a/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap new file mode 100644 index 0000000000000..2227cbe8a495c --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccessAgreementPage renders as expected when state is available 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" + > + <p + key="paragraph-1-1" + > + This is + <a + href="../link" + key="link-1-9" + > + link + </a> + </p> + </div> +</ReactMarkdown> +`; diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts new file mode 100644 index 0000000000000..add2db6a3c170 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./access_agreement_page'); + +import { AppMount, ScopedHistory } from 'src/core/public'; +import { accessAgreementApp } from './access_agreement_app'; + +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; + +describe('accessAgreementApp', () => { + it('properly registers application', () => { + const coreSetupMock = coreMock.createSetup(); + + accessAgreementApp.create({ + application: coreSetupMock.application, + getStartServices: coreSetupMock.getStartServices, + }); + + expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1); + + const [[appRegistration]] = coreSetupMock.application.register.mock.calls; + expect(appRegistration).toEqual({ + id: 'security_access_agreement', + chromeless: true, + appRoute: '/security/access_agreement', + title: 'Access Agreement', + mount: expect.any(Function), + }); + }); + + it('properly renders application', async () => { + const coreSetupMock = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]); + const containerMock = document.createElement('div'); + + accessAgreementApp.create({ + application: coreSetupMock.application, + getStartServices: coreSetupMock.getStartServices, + }); + + const [[{ mount }]] = coreSetupMock.application.register.mock.calls; + await (mount as AppMount)({ + element: containerMock, + appBasePath: '', + onAppLeave: jest.fn(), + history: (scopedHistoryMock.create() as unknown) as ScopedHistory, + }); + + const mockRenderApp = jest.requireMock('./access_agreement_page').renderAccessAgreementPage; + expect(mockRenderApp).toHaveBeenCalledTimes(1); + expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, { + http: coreStartMock.http, + notifications: coreStartMock.notifications, + fatalErrors: coreStartMock.fatalErrors, + }); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts new file mode 100644 index 0000000000000..156a76542a28f --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; + +interface CreateDeps { + application: ApplicationSetup; + getStartServices: StartServicesAccessor; +} + +export const accessAgreementApp = Object.freeze({ + id: 'security_access_agreement', + create({ application, getStartServices }: CreateDeps) { + application.register({ + id: this.id, + title: i18n.translate('xpack.security.accessAgreementAppTitle', { + defaultMessage: 'Access Agreement', + }), + chromeless: true, + appRoute: '/security/access_agreement', + async mount({ element }: AppMountParameters) { + const [[coreStart], { renderAccessAgreementPage }] = await Promise.all([ + getStartServices(), + import('./access_agreement_page'), + ]); + return renderAccessAgreementPage(coreStart.i18n, element, { + http: coreStart.http, + notifications: coreStart.notifications, + fatalErrors: coreStart.fatalErrors, + }); + }, + }); + }, +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss new file mode 100644 index 0000000000000..08e7be248619f --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss @@ -0,0 +1,21 @@ +.secAccessAgreementPage .secAuthenticationStatePage__content { + max-width: 600px; +} + +.secAccessAgreementPage__textWrapper { + overflow-y: hidden; +} + +.secAccessAgreementPage__text { + @include euiYScrollWithShadows; + max-height: 400px; + padding: $euiSize $euiSizeL 0; +} + +.secAccessAgreementPage__footer { + padding: $euiSize $euiSizeL $euiSizeL; +} + +.secAccessAgreementPage__footerInner { + text-align: left; +} diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx new file mode 100644 index 0000000000000..89b7489d45ebb --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import { EuiLoadingContent } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { AccessAgreementPage } from './access_agreement_page'; + +describe('AccessAgreementPage', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { href: 'http://some-host/bar', protocol: 'http' }, + writable: true, + }); + }); + + afterAll(() => { + delete (window as any).location; + }); + + it('renders as expected when state is available', async () => { + const coreStartMock = coreMock.createStart(); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + expect(wrapper.exists(EuiLoadingContent)).toBe(true); + expect(wrapper.exists(ReactMarkdown)).toBe(false); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(ReactMarkdown)).toMatchSnapshot(); + expect(wrapper.exists(EuiLoadingContent)).toBe(false); + + expect(coreStartMock.http.get).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.get).toHaveBeenCalledWith( + '/internal/security/access_agreement/state' + ); + expect(coreStartMock.fatalErrors.add).not.toHaveBeenCalled(); + }); + + it('fails when state is not available', async () => { + const coreStartMock = coreMock.createStart(); + const error = Symbol(); + coreStartMock.http.get.mockRejectedValue(error); + + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.get).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.get).toHaveBeenCalledWith( + '/internal/security/access_agreement/state' + ); + expect(coreStartMock.fatalErrors.add).toHaveBeenCalledTimes(1); + expect(coreStartMock.fatalErrors.add).toHaveBeenCalledWith(error); + }); + + it('properly redirects after successful acknowledgement', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + coreStartMock.http.post.mockResolvedValue(undefined); + + window.location.href = `https://some-host/security/access_agreement?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + findTestSubject(wrapper, 'accessAgreementAcknowledge').simulate('click'); + + await act(async () => { + await nextTick(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith( + '/internal/security/access_agreement/acknowledge' + ); + + expect(window.location.href).toBe('/some-base-path/app/kibana#/home?_g=()'); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('shows error toast if acknowledgement fails', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const failureReason = new Error('Oh no!'); + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + coreStartMock.http.post.mockRejectedValue(failureReason); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + findTestSubject(wrapper, 'accessAgreementAcknowledge').simulate('click'); + + await act(async () => { + await nextTick(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith( + '/internal/security/access_agreement/acknowledge' + ); + + expect(window.location.href).toBe(currentURL); + expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith(failureReason, { + title: 'Could not acknowledge access agreement.', + }); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx new file mode 100644 index 0000000000000..a34dcb18d2b9c --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './access_agreement_page.scss'; + +import React, { FormEvent, MouseEvent, useCallback, useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import ReactMarkdown from 'react-markdown'; +import { + EuiButton, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public'; + +import { parseNext } from '../../../common/parse_next'; +import { AuthenticationStatePage } from '../components'; + +interface Props { + http: HttpStart; + notifications: NotificationsStart; + fatalErrors: FatalErrorsStart; +} + +export function AccessAgreementPage({ http, fatalErrors, notifications }: Props) { + const [isLoading, setIsLoading] = useState<boolean>(false); + + const [accessAgreement, setAccessAgreement] = useState<string | null>(null); + useEffect(() => { + http + .get<{ accessAgreement: string }>('/internal/security/access_agreement/state') + .then(response => setAccessAgreement(response.accessAgreement)) + .catch(err => fatalErrors.add(err)); + }, [http, fatalErrors]); + + const onAcknowledge = useCallback( + async (e: MouseEvent<HTMLButtonElement> | FormEvent<HTMLFormElement>) => { + e.preventDefault(); + + try { + setIsLoading(true); + await http.post('/internal/security/access_agreement/acknowledge'); + window.location.href = parseNext(window.location.href, http.basePath.serverBasePath); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.security.accessAgreement.acknowledgeErrorMessage', { + defaultMessage: 'Could not acknowledge access agreement.', + }), + }); + + setIsLoading(false); + } + }, + [http, notifications] + ); + + const content = accessAgreement ? ( + <form onSubmit={onAcknowledge}> + <EuiPanel paddingSize="none"> + <EuiFlexGroup gutterSize="none" direction="column"> + <EuiFlexItem className="secAccessAgreementPage__textWrapper"> + <div className="secAccessAgreementPage__text"> + <EuiText textAlign="left"> + <ReactMarkdown>{accessAgreement}</ReactMarkdown> + </EuiText> + </div> + </EuiFlexItem> + <EuiFlexItem className="secAccessAgreementPage__footer"> + <div className="secAccessAgreementPage__footerInner"> + <EuiButton + fill + type="submit" + color="primary" + onClick={onAcknowledge} + isDisabled={isLoading} + isLoading={isLoading} + data-test-subj="accessAgreementAcknowledge" + > + <FormattedMessage + id="xpack.security.accessAgreement.acknowledgeButtonText" + defaultMessage="Acknowledge and continue" + /> + </EuiButton> + </div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </form> + ) : ( + <EuiPanel paddingSize="l"> + <EuiLoadingContent lines={10} /> + </EuiPanel> + ); + + return ( + <AuthenticationStatePage + className="secAccessAgreementPage" + title={ + <FormattedMessage + id="xpack.security.accessAgreement.title" + defaultMessage="Access Agreement" + /> + } + > + {content} + <EuiSpacer size="xxl" /> + </AuthenticationStatePage> + ); +} + +export function renderAccessAgreementPage( + i18nStart: CoreStart['i18n'], + element: Element, + props: Props +) { + ReactDOM.render( + <i18nStart.Context> + <AccessAgreementPage {...props} /> + </i18nStart.Context>, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +} diff --git a/x-pack/plugins/security/public/authentication/access_agreement/index.ts b/x-pack/plugins/security/public/authentication/access_agreement/index.ts new file mode 100644 index 0000000000000..8f7661a89a269 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { accessAgreementApp } from './access_agreement_app'; diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 2e73b8cd04482..6657f5c0a900c 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -8,6 +8,7 @@ import { ApplicationSetup, StartServicesAccessor, HttpSetup } from 'src/core/pub import { AuthenticatedUser } from '../../common/model'; import { ConfigType } from '../config'; import { PluginStartDependencies } from '../plugin'; +import { accessAgreementApp } from './access_agreement'; import { loginApp } from './login'; import { logoutApp } from './logout'; import { loggedOutApp } from './logged_out'; @@ -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 dfa258d523c5a..0000000000000 --- a/x-pack/plugins/security/public/authentication/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './authentication_state_page/index'; diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap index 3590fa460a401..585dc368da707 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap @@ -2,7 +2,7 @@ exports[`AuthenticationStatePage renders 1`] = ` <div - className="secAuthenticationStatePage" + className="secAuthenticationStatePage " > <header className="secAuthenticationStatePage__header" @@ -18,7 +18,7 @@ exports[`AuthenticationStatePage renders 1`] = ` > <EuiIcon size="xxl" - type="logoKibana" + type="logoElastic" /> </span> <EuiTitle diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss deleted file mode 100644 index f7cdd75143791..0000000000000 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './authentication_state_page'; diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss rename to x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx index e1292c5b21536..946c58a1c8e99 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx @@ -18,4 +18,14 @@ describe('AuthenticationStatePage', () => { ) ).toMatchSnapshot(); }); + + it('renders with custom CSS class', () => { + expect( + shallowWithIntl( + <AuthenticationStatePage className="customClassName" title={'foo'}> + <span>hello world</span> + </AuthenticationStatePage> + ).exists('.secAuthenticationStatePage.customClassName') + ).toBe(true); + }); }); diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx index aa30661129978..35be650d127fb 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx @@ -4,20 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import './authentication_state_page.scss'; + import { EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; interface Props { + className?: string; title: React.ReactNode; } export const AuthenticationStatePage: React.FC<Props> = props => ( - <div className="secAuthenticationStatePage"> + <div className={`secAuthenticationStatePage ${props.className || ''}`}> <header className="secAuthenticationStatePage__header"> <div className="secAuthenticationStatePage__content eui-textCenter"> <EuiSpacer size="xxl" /> <span className="secAuthenticationStatePage__logo"> - <EuiIcon type="logoKibana" size="xxl" /> + <EuiIcon type="logoElastic" size="xxl" /> </span> <EuiTitle size="l" className="secAuthenticationStatePage__title"> <h1>{props.title}</h1> diff --git a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap index ecbdfedac1dd3..bbc6bfa1faddc 100644 --- a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap @@ -121,10 +121,15 @@ exports[`LoginPage enabled form state renders as expected 1`] = ` selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -155,10 +160,15 @@ exports[`LoginPage enabled form state renders as expected when info message is s selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -189,10 +199,55 @@ exports[`LoginPage enabled form state renders as expected when loginAssistanceMe selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], + } + } +/> +`; + +exports[`LoginPage enabled form state renders as expected when loginHelp is set 1`] = ` +<LoginForm + http={ + Object { + "addLoadingCountSource": [MockFunction], + "get": [MockFunction], + } + } + infoMessage="Your session has timed out. Please log in again." + loginAssistanceMessage="" + loginHelp="**some-help**" + notifications={ + Object { + "toasts": Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + }, + } + } + selector={ + Object { + "enabled": false, + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -279,10 +334,15 @@ exports[`LoginPage page renders as expected 1`] = ` selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security/public/authentication/login/_index.scss b/x-pack/plugins/security/public/authentication/login/_index.scss deleted file mode 100644 index 4dd2c0cabfb5e..0000000000000 --- a/x-pack/plugins/security/public/authentication/login/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './login_page'; diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap index 7b8283b7bec0e..072a025aa06a0 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap @@ -1,170 +1,91 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LoginForm login selector renders as expected with login form 1`] = ` -<Fragment> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="saml1" - onClick={[Function]} - > - Login w/SAML - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="pki1" - onClick={[Function]} - > - Login w/PKI - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiText - color="subdued" - textAlign="center" +exports[`LoginForm login selector properly switches to login form -> login help and back: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - ―――   - <FormattedMessage - defaultMessage="OR" - id="xpack.security.loginPage.loginSelectorOR" - values={Object {}} - /> -   ――― - </EuiText> - <EuiSpacer - size="m" - /> - <EuiPanel> - <form - onSubmit={[Function]} + <p + key="paragraph-1-1" > - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - isInvalid={false} - label={ - <FormattedMessage - defaultMessage="Username" - id="xpack.security.login.basicLoginForm.usernameFormRowLabel" - values={Object {}} - /> - } - labelType="label" + <strong + key="strong-1-1" > - <EuiFieldText - aria-required={true} - data-test-subj="loginUsername" - disabled={false} - id="username" - inputRef={[Function]} - isInvalid={false} - name="username" - onChange={[Function]} - value="" - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - isInvalid={false} - label={ - <FormattedMessage - defaultMessage="Password" - id="xpack.security.login.basicLoginForm.passwordFormRowLabel" - values={Object {}} - /> - } - labelType="label" - > - <EuiFieldPassword - aria-required={true} - autoComplete="off" - compressed={false} - data-test-subj="loginPassword" - disabled={false} - fullWidth={false} - id="password" - isInvalid={false} - isLoading={false} - name="password" - onChange={[Function]} - value="" - /> - </EuiFormRow> - <EuiButton - color="primary" - data-test-subj="loginSubmit" - fill={true} - isDisabled={false} - isLoading={false} - onClick={[Function]} - type="submit" - > - <FormattedMessage - defaultMessage="Log in" - id="xpack.security.login.basicLoginForm.logInButtonLabel" - values={Object {}} - /> - </EuiButton> - </form> - </EuiPanel> -</Fragment> + some help + </strong> + </p> + </div> +</ReactMarkdown> `; -exports[`LoginForm login selector renders as expected without login form for providers with and without description 1`] = ` -<Fragment> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="saml1" - onClick={[Function]} +exports[`LoginForm login selector properly switches to login help: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - Login w/SAML - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="pki1" - onClick={[Function]} + <p + key="paragraph-1-1" + > + <strong + key="strong-1-1" + > + some help + </strong> + </p> + </div> +</ReactMarkdown> +`; + +exports[`LoginForm properly switches to login help: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - <FormattedMessage - defaultMessage="Login with {providerType}/{providerName}" - id="xpack.security.loginPage.loginProviderDescription" - values={ - Object { - "providerName": "pki1", - "providerType": "pki", - } - } - /> - </EuiButton> - <EuiSpacer - size="m" - /> -</Fragment> + <p + key="paragraph-1-1" + > + <strong + key="strong-1-1" + > + some help + </strong> + </p> + </div> +</ReactMarkdown> `; exports[`LoginForm renders as expected 1`] = ` <Fragment> - <EuiPanel> + <EuiPanel + data-test-subj="loginForm" + > <form onSubmit={[Function]} > @@ -227,21 +148,32 @@ exports[`LoginForm renders as expected 1`] = ` value="" /> </EuiFormRow> - <EuiButton - color="primary" - data-test-subj="loginSubmit" - fill={true} - isDisabled={false} - isLoading={false} - onClick={[Function]} - type="submit" + <EuiSpacer /> + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} > - <FormattedMessage - defaultMessage="Log in" - id="xpack.security.login.basicLoginForm.logInButtonLabel" - values={Object {}} - /> - </EuiButton> + <EuiFlexItem + grow={false} + > + <EuiButton + color="primary" + data-test-subj="loginSubmit" + fill={true} + isDisabled={false} + isLoading={false} + onClick={[Function]} + type="submit" + > + <FormattedMessage + defaultMessage="Log in" + id="xpack.security.login.basicLoginForm.logInButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> </form> </EuiPanel> </Fragment> diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss new file mode 100644 index 0000000000000..6784052ef4337 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss @@ -0,0 +1,55 @@ +.secLoginCard { + display: block; + box-shadow: none; + padding: $euiSize; + text-align: left; + width: 100%; + + &:hover { + .secLoginCard__title { + text-decoration: underline; + } + } + + &:disabled { + pointer-events: none; + } + + &:not(.secLoginCard-isLoading):disabled { + .secLoginCard__title, + .secLoginCard__hint { + color: $euiColorMediumShade; + } + } + + &:focus { + border-color: transparent; + border-radius: $euiBorderRadius; + @include euiFocusRing; + + .secLoginCard__title { + text-decoration: underline; + } + + // Make the focus ring clean and without borders + + .secLoginCard { + border-color: transparent; + } + } + + + .secLoginCard { + border-top: $euiBorderThin; + } +} + +.secLoginCard__hint { + @include euiFontSizeXS; + color: $euiColorDarkShade; + margin-top: $euiSizeXS; +} + +.secLoginAssistanceMessage { + // This tightens up the layout if message is present + margin-top: -($euiSizeXXL + $euiSizeS); + padding: 0 $euiSize; +} diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx index c17c10a2c5148..4e172cdde0eed 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx @@ -5,12 +5,39 @@ */ import React from 'react'; +import ReactMarkdown from 'react-markdown'; import { act } from '@testing-library/react'; -import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiIcon } from '@elastic/eui'; import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LoginForm } from './login_form'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { LoginForm, PageMode } from './login_form'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { ReactWrapper } from 'enzyme'; + +function expectPageMode(wrapper: ReactWrapper, mode: PageMode) { + const assertions: Array<[string, boolean]> = + mode === PageMode.Form + ? [ + ['loginForm', true], + ['loginSelector', false], + ['loginHelp', false], + ] + : mode === PageMode.Selector + ? [ + ['loginForm', false], + ['loginSelector', true], + ['loginHelp', false], + ] + : [ + ['loginForm', false], + ['loginSelector', false], + ['loginHelp', true], + ]; + for (const [selector, exists] of assertions) { + expect(findTestSubject(wrapper, selector).exists()).toBe(exists); + } +} describe('LoginForm', () => { beforeAll(() => { @@ -32,8 +59,10 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ) ).toMatchSnapshot(); @@ -41,20 +70,44 @@ describe('LoginForm', () => { it('renders an info message when provided.', () => { const coreStartMock = coreMock.createStart(); - const wrapper = shallowWithIntl( + const wrapper = mountWithIntl( <LoginForm http={coreStartMock.http} notifications={coreStartMock.notifications} infoMessage={'Hey this is an info message'} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + expect(wrapper.find(EuiCallOut).props().title).toEqual('Hey this is an info message'); }); + it('renders `Need help?` link if login help text is provided.', () => { + const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="" + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Form); + + expect(findTestSubject(wrapper, 'loginHelpLink').text()).toEqual('Need help?'); + }); + it('renders an invalid credentials message', async () => { const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); coreStartMock.http.post.mockRejectedValue({ response: { status: 401 } }); @@ -64,11 +117,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } }); wrapper.find(EuiButton).simulate('click'); @@ -92,11 +149,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } }); wrapper.find(EuiButton).simulate('click'); @@ -121,11 +182,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username1' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password1' } }); wrapper.find(EuiButton).simulate('click'); @@ -144,47 +209,125 @@ describe('LoginForm', () => { expect(wrapper.find(EuiCallOut).exists()).toBe(false); }); + it('properly switches to login help', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Form); + expect(findTestSubject(wrapper, 'loginBackToSelector').exists()).toBe(false); + + // Going to login help. + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot('Login Help'); + + // Going back to login form. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + expect(findTestSubject(wrapper, 'loginBackToSelector').exists()).toBe(false); + }); + describe('login selector', () => { - it('renders as expected with login form', async () => { + it('renders as expected with providers that use login form', async () => { const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { + type: 'basic', + name: 'basic', + usesLoginForm: true, + hint: 'Basic hint', + icon: 'logoElastic', + }, + { type: 'saml', name: 'saml1', description: 'Log in w/SAML', usesLoginForm: false }, + { + type: 'pki', + name: 'pki1', + description: 'Log in w/PKI', + hint: 'PKI hint', + usesLoginForm: false, + }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + expect( - shallowWithIntl( - <LoginForm - http={coreStartMock.http} - notifications={coreStartMock.notifications} - loginAssistanceMessage="" - showLoginForm={true} - selector={{ - enabled: true, - providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1', description: 'Login w/PKI' }, - ], - }} - /> - ) - ).toMatchSnapshot(); + wrapper.find('.secLoginCard').map(card => { + const hint = card.find('.secLoginCard__hint'); + return { + title: card.find('p.secLoginCard__title').text(), + hint: hint.exists() ? hint.text() : '', + icon: card.find(EuiIcon).props().type, + }; + }) + ).toEqual([ + { title: 'Log in with basic/basic', hint: 'Basic hint', icon: 'logoElastic' }, + { title: 'Log in w/SAML', hint: '', icon: 'empty' }, + { title: 'Log in w/PKI', hint: 'PKI hint', icon: 'empty' }, + ]); }); - it('renders as expected without login form for providers with and without description', async () => { + it('renders as expected without providers that use login form', async () => { const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { + type: 'saml', + name: 'saml1', + description: 'Login w/SAML', + hint: 'SAML hint', + usesLoginForm: false, + }, + { type: 'pki', name: 'pki1', icon: 'some-icon', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + expect( - shallowWithIntl( - <LoginForm - http={coreStartMock.http} - notifications={coreStartMock.notifications} - loginAssistanceMessage="" - showLoginForm={false} - selector={{ - enabled: true, - providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1' }, - ], - }} - /> - ) - ).toMatchSnapshot(); + wrapper.find('.secLoginCard').map(card => { + const hint = card.find('.secLoginCard__hint'); + return { + title: card.find('p.secLoginCard__title').text(), + hint: hint.exists() ? hint.text() : '', + icon: card.find(EuiIcon).props().type, + }; + }) + ).toEqual([ + { title: 'Login w/SAML', hint: 'SAML hint', icon: 'empty' }, + { title: 'Log in with pki/pki1', hint: '', icon: 'some-icon' }, + ]); }); it('properly redirects after successful login', async () => { @@ -203,17 +346,19 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} selector={{ enabled: true, providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1', description: 'Login w/PKI' }, + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', description: 'Login w/SAML', usesLoginForm: false }, + { type: 'pki', name: 'pki1', description: 'Login w/PKI', usesLoginForm: false }, ], }} /> ); + expectPageMode(wrapper, PageMode.Selector); + wrapper.findWhere(node => node.key() === 'saml1').simulate('click'); await act(async () => { @@ -246,11 +391,18 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: true, providers: [{ type: 'saml', name: 'saml1' }] }} + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} /> ); + expectPageMode(wrapper, PageMode.Selector); + wrapper.findWhere(node => node.key() === 'saml1').simulate('click'); await act(async () => { @@ -268,5 +420,123 @@ describe('LoginForm', () => { title: 'Could not perform login.', }); }); + + it('properly switches to login form', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + window.location.href = currentURL; + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + wrapper.findWhere(node => node.key() === 'basic').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + expect(window.location.href).toBe(currentURL); + }); + + it('properly switches to login help', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot( + 'Login Help' + ); + + // Going back to login selector. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Selector); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('properly switches to login form -> login help and back', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + // Going to login form. + wrapper.findWhere(node => node.key() === 'basic').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + // Going to login help. + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot( + 'Login Help' + ); + + // Going back to login form. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + // Going back to login selector. + findTestSubject(wrapper, 'loginBackToSelector').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Selector); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index 01f5c40a69aeb..460c6550085a4 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import './login_form.scss'; + import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import ReactMarkdown from 'react-markdown'; import { EuiButton, + EuiIcon, EuiCallOut, EuiFieldPassword, EuiFieldText, @@ -15,21 +18,28 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiLoadingSpinner, + EuiLink, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public'; -import { LoginValidator, LoginValidationResult } from './validate_login'; import { parseNext } from '../../../../../common/parse_next'; import { LoginSelector } from '../../../../../common/login_state'; +import { LoginValidator } from './validate_login'; interface Props { http: HttpStart; notifications: NotificationsStart; selector: LoginSelector; - showLoginForm: boolean; infoMessage?: string; loginAssistanceMessage: string; + loginHelp?: string; } interface State { @@ -42,7 +52,8 @@ interface State { message: | { type: MessageType.None } | { type: MessageType.Danger | MessageType.Info; content: string }; - formError: LoginValidationResult | null; + mode: PageMode; + previousMode: PageMode; } enum LoadingStateType { @@ -57,12 +68,21 @@ enum MessageType { Danger, } +export enum PageMode { + Selector, + Form, + LoginHelp, +} + export class LoginForm extends Component<Props, State> { private readonly validator: LoginValidator; constructor(props: Props) { super(props); this.validator = new LoginValidator({ shouldValidate: false }); + + const mode = this.showLoginSelector() ? PageMode.Selector : PageMode.Form; + this.state = { loadingState: { type: LoadingStateType.None }, username: '', @@ -70,7 +90,8 @@ export class LoginForm extends Component<Props, State> { message: this.props.infoMessage ? { type: MessageType.Info, content: this.props.infoMessage } : { type: MessageType.None }, - formError: null, + mode, + previousMode: mode, }; } @@ -79,19 +100,91 @@ export class LoginForm extends Component<Props, State> { <Fragment> {this.renderLoginAssistanceMessage()} {this.renderMessage()} - {this.renderSelector()} - {this.renderLoginForm()} + {this.renderContent()} + {this.renderPageModeSwitchLink()} </Fragment> ); } - private renderLoginForm = () => { - if (!this.props.showLoginForm) { + private renderLoginAssistanceMessage = () => { + if (!this.props.loginAssistanceMessage) { return null; } return ( - <EuiPanel> + <div className="secLoginAssistanceMessage"> + <EuiHorizontalRule size="half" /> + <EuiText size="xs"> + <ReactMarkdown>{this.props.loginAssistanceMessage}</ReactMarkdown> + </EuiText> + </div> + ); + }; + + private renderMessage = () => { + const { message } = this.state; + if (message.type === MessageType.Danger) { + return ( + <Fragment> + <EuiCallOut + size="s" + color="danger" + data-test-subj="loginErrorMessage" + title={message.content} + role="alert" + /> + <EuiSpacer size="l" /> + </Fragment> + ); + } + + if (message.type === MessageType.Info) { + return ( + <Fragment> + <EuiCallOut + size="s" + color="primary" + data-test-subj="loginInfoMessage" + title={message.content} + role="status" + /> + <EuiSpacer size="l" /> + </Fragment> + ); + } + + return null; + }; + + public renderContent() { + switch (this.state.mode) { + case PageMode.Form: + return this.renderLoginForm(); + case PageMode.Selector: + return this.renderSelector(); + case PageMode.LoginHelp: + return this.renderLoginHelp(); + } + } + + private renderLoginForm = () => { + const loginSelectorLink = this.showLoginSelector() ? ( + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="loginBackToSelector" + size="xs" + onClick={() => this.onPageModeChange(PageMode.Selector)} + > + <FormattedMessage + id="xpack.security.loginPage.loginSelectorLinkText" + defaultMessage="See more login options" + /> + </EuiButtonEmpty> + </EuiFlexItem> + ) : null; + + return ( + <EuiPanel data-test-subj="loginForm"> <form onSubmit={this.submitLoginForm}> <EuiFormRow label={ @@ -137,67 +230,128 @@ export class LoginForm extends Component<Props, State> { /> </EuiFormRow> - <EuiButton - fill - type="submit" - color="primary" - onClick={this.submitLoginForm} - isDisabled={!this.isLoadingState(LoadingStateType.None)} - isLoading={this.isLoadingState(LoadingStateType.Form)} - data-test-subj="loginSubmit" - > - <FormattedMessage - id="xpack.security.login.basicLoginForm.logInButtonLabel" - defaultMessage="Log in" - /> - </EuiButton> + <EuiSpacer /> + + <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButton + fill + type="submit" + color="primary" + onClick={this.submitLoginForm} + isDisabled={!this.isLoadingState(LoadingStateType.None)} + isLoading={this.isLoadingState(LoadingStateType.Form)} + data-test-subj="loginSubmit" + > + <FormattedMessage + id="xpack.security.login.basicLoginForm.logInButtonLabel" + defaultMessage="Log in" + /> + </EuiButton> + </EuiFlexItem> + {loginSelectorLink} + </EuiFlexGroup> </form> </EuiPanel> ); }; - private renderLoginAssistanceMessage = () => { - if (!this.props.loginAssistanceMessage) { - return null; - } + private renderSelector = () => { + return ( + <EuiPanel data-test-subj="loginSelector" paddingSize="none"> + {this.props.selector.providers.map(provider => ( + <button + key={provider.name} + disabled={!this.isLoadingState(LoadingStateType.None)} + onClick={() => + provider.usesLoginForm + ? this.onPageModeChange(PageMode.Form) + : this.loginWithSelector(provider.type, provider.name) + } + className={`secLoginCard ${ + this.isLoadingState(LoadingStateType.Selector, provider.name) + ? 'secLoginCard-isLoading' + : '' + }`} + > + <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}> + <EuiFlexItem grow={false}> + <EuiIcon size="xl" type={provider.icon ? provider.icon : 'empty'} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size="xs" className="secLoginCard__title"> + <p> + {provider.description ?? ( + <FormattedMessage + id="xpack.security.loginPage.loginProviderDescription" + defaultMessage="Log in with {providerType}/{providerName}" + values={{ + providerType: provider.type, + providerName: provider.name, + }} + /> + )} + </p> + </EuiTitle> + {provider.hint ? <p className="secLoginCard__hint">{provider.hint}</p> : null} + </EuiFlexItem> + {this.isLoadingState(LoadingStateType.Selector, provider.name) ? ( + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </button> + ))} + </EuiPanel> + ); + }; + private renderLoginHelp = () => { return ( - <Fragment> - <EuiText size="s"> - <ReactMarkdown>{this.props.loginAssistanceMessage}</ReactMarkdown> + <EuiPanel data-test-subj="loginHelp"> + <EuiText> + <ReactMarkdown>{this.props.loginHelp || ''}</ReactMarkdown> </EuiText> - </Fragment> + </EuiPanel> ); }; - private renderMessage = () => { - const { message } = this.state; - if (message.type === MessageType.Danger) { + private renderPageModeSwitchLink = () => { + if (this.state.mode === PageMode.LoginHelp) { return ( <Fragment> - <EuiCallOut - size="s" - color="danger" - data-test-subj="loginErrorMessage" - title={message.content} - role="alert" - /> - <EuiSpacer size="l" /> + <EuiSpacer /> + <EuiText size="xs" className="eui-textCenter"> + <EuiLink + data-test-subj="loginBackToLoginLink" + onClick={() => this.onPageModeChange(this.state.previousMode)} + > + <FormattedMessage + id="xpack.security.loginPage.goBackToLoginLink" + defaultMessage="Take me back to Login" + /> + </EuiLink> + </EuiText> </Fragment> ); } - if (message.type === MessageType.Info) { + if (this.props.loginHelp) { return ( <Fragment> - <EuiCallOut - size="s" - color="primary" - data-test-subj="loginInfoMessage" - title={message.content} - role="status" - /> - <EuiSpacer size="l" /> + <EuiSpacer /> + <EuiText size="xs" className="eui-textCenter"> + <EuiLink + data-test-subj="loginHelpLink" + onClick={() => this.onPageModeChange(PageMode.LoginHelp)} + > + <FormattedMessage + id="xpack.security.loginPage.loginHelpLinkText" + defaultMessage="Need help?" + /> + </EuiLink> + </EuiText> </Fragment> ); } @@ -205,60 +359,16 @@ export class LoginForm extends Component<Props, State> { return null; }; - private renderSelector = () => { - const showLoginSelector = - this.props.selector.enabled && this.props.selector.providers.length > 0; - if (!showLoginSelector) { - return null; - } - - const loginSelectorAndLoginFormSeparator = showLoginSelector && this.props.showLoginForm && ( - <> - <EuiText textAlign="center" color="subdued"> - ―――   - <FormattedMessage id="xpack.security.loginPage.loginSelectorOR" defaultMessage="OR" /> -   ――― - </EuiText> - <EuiSpacer size="m" /> - </> - ); - - return ( - <> - {this.props.selector.providers.map((provider, index) => ( - <Fragment key={index}> - <EuiButton - key={provider.name} - fullWidth={true} - isDisabled={!this.isLoadingState(LoadingStateType.None)} - isLoading={this.isLoadingState(LoadingStateType.Selector, provider.name)} - onClick={() => this.loginWithSelector(provider.type, provider.name)} - > - {provider.description ?? ( - <FormattedMessage - id="xpack.security.loginPage.loginProviderDescription" - defaultMessage="Login with {providerType}/{providerName}" - values={{ - providerType: provider.type, - providerName: provider.name, - }} - /> - )} - </EuiButton> - <EuiSpacer size="m" /> - </Fragment> - ))} - {loginSelectorAndLoginFormSeparator} - </> - ); - }; - private setUsernameInputRef(ref: HTMLInputElement) { if (ref) { ref.focus(); } } + private onPageModeChange = (mode: PageMode) => { + this.setState({ message: { type: MessageType.None }, mode, previousMode: this.state.mode }); + }; + private onUsernameChange = (e: ChangeEvent<HTMLInputElement>) => { this.setState({ username: e.target.value, @@ -279,12 +389,10 @@ export class LoginForm extends Component<Props, State> { this.validator.enableValidation(); const { username, password } = this.state; - const result = this.validator.validateForLogin(username, password); - if (result.isInvalid) { - this.setState({ formError: result }); - return; - } else { - this.setState({ formError: null }); + if (this.validator.validateForLogin(username, password).isInvalid) { + // Since validation is enabled now, we should ask React to re-render form and display + // validation error messages if any. + return this.forceUpdate(); } this.setState({ @@ -351,4 +459,11 @@ export class LoginForm extends Component<Props, State> { loadingState.type !== LoadingStateType.Selector || loadingState.providerName === providerName ); } + + private showLoginSelector() { + return ( + this.props.selector.enabled && + this.props.selector.providers.some(provider => !provider.usesLoginForm) + ); + } } diff --git a/x-pack/plugins/security/public/authentication/login/_login_page.scss b/x-pack/plugins/security/public/authentication/login/login_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/login/_login_page.scss rename to x-pack/plugins/security/public/authentication/login/login_page.scss diff --git a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx index c4be57d8d7db7..ab107e46dfff6 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx @@ -18,8 +18,10 @@ const createLoginState = (options?: Partial<LoginState>) => { allowLogin: true, layout: 'form', requiresSecureConnection: false, - showLoginForm: true, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ type: 'basic', name: 'basic1', usesLoginForm: true }], + }, ...options, } as LoginState; }; @@ -163,7 +165,9 @@ describe('LoginPage', () => { it('renders as expected when login is not enabled', async () => { const coreStartMock = coreMock.createStart(); - httpMock.get.mockResolvedValue(createLoginState({ showLoginForm: false })); + httpMock.get.mockResolvedValue( + createLoginState({ selector: { enabled: false, providers: [] } }) + ); const wrapper = shallow( <LoginPage @@ -250,6 +254,28 @@ describe('LoginPage', () => { expect(wrapper.find(LoginForm)).toMatchSnapshot(); }); + + it('renders as expected when loginHelp is set', async () => { + const coreStartMock = coreMock.createStart(); + httpMock.get.mockResolvedValue(createLoginState({ loginHelp: '**some-help**' })); + + const wrapper = shallow( + <LoginPage + http={httpMock} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + loginAssistanceMessage="" + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot + }); + + expect(wrapper.find(LoginForm)).toMatchSnapshot(); + }); }); describe('API calls', () => { diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 70f8f76ee0a9c..d24a301ed24ec 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './login_page.scss'; + import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; @@ -120,10 +122,9 @@ export class LoginPage extends Component<Props, State> { requiresSecureConnection, isSecureConnection, selector, - showLoginForm, + loginHelp, }: LoginState & { isSecureConnection: boolean }) => { - const isLoginExplicitlyDisabled = - !showLoginForm && (!selector.enabled || selector.providers.length === 0); + const isLoginExplicitlyDisabled = selector.providers.length === 0; if (isLoginExplicitlyDisabled) { return ( <DisabledLoginForm @@ -223,10 +224,10 @@ export class LoginPage extends Component<Props, State> { <LoginForm http={this.props.http} notifications={this.props.notifications} - showLoginForm={showLoginForm} selector={selector} infoMessage={infoMessageMap.get(parse(window.location.href, true).query.msg?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} + loginHelp={loginHelp} /> ); }; diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap index 2ff760891fa4e..02b1a7d0d3fa0 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap @@ -11,7 +11,7 @@ exports[`OverwrittenSessionPage renders as expected 1`] = ` } > <div - className="secAuthenticationStatePage" + className="secAuthenticationStatePage " > <header className="secAuthenticationStatePage__header" @@ -31,10 +31,10 @@ exports[`OverwrittenSessionPage renders as expected 1`] = ` > <EuiIcon size="xxl" - type="logoKibana" + type="logoElastic" > <div - data-euiicon-type="logoKibana" + data-euiicon-type="logoElastic" size="xxl" /> </EuiIcon> diff --git a/x-pack/plugins/security/public/index.scss b/x-pack/plugins/security/public/index.scss deleted file mode 100644 index 999639ba22eb7..0000000000000 --- a/x-pack/plugins/security/public/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -$secFormWidth: 460px; - -// Authentication styles -@import './authentication/index'; - -// Management styles -@import './management/index'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 458f7ab801fdf..8016c94224060 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './index.scss'; import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; import { SecurityPlugin, @@ -15,7 +14,6 @@ import { } from './plugin'; export { SecurityPluginSetup, SecurityPluginStart }; -export { SessionInfo } from './types'; export { AuthenticatedUser } from '../common/model'; export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing'; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss deleted file mode 100644 index 5d419b5323079..0000000000000 --- a/x-pack/plugins/security/public/management/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './roles/index'; -@import './users/index'; -@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 272fc9cfc2fe6..b9ec5b35b3f9d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -10,8 +10,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { APIKeysGridPage } from './api_keys_grid'; -import { APIKeysAPIClient } from './api_keys_api_client'; import { DocumentationLinksService } from './documentation_links'; interface CreateParams { @@ -28,7 +26,6 @@ export const apiKeysManagementApp = Object.freeze({ defaultMessage: 'API Keys', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); setBreadcrumbs([ { text: i18n.translate('xpack.security.apiKeys.breadcrumb', { @@ -38,6 +35,16 @@ export const apiKeysManagementApp = Object.freeze({ }, ]); + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { APIKeysGridPage }, + { APIKeysAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./api_keys_grid'), + import('./api_keys_api_client'), + ]); + render( <i18nStart.Context> <APIKeysGridPage diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss deleted file mode 100644 index bae6effcd2ec5..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_role_mapping/index'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 3f240b8a2b2a2..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './rule_editor_panel/index'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss deleted file mode 100644 index c3b2764e64713..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './rule_editor_group'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.scss similarity index 100% rename from x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.scss diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index c17a853a65467..b10a6dd8d183f 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './rule_group_editor.scss'; + import React, { Component, Fragment } from 'react'; import { EuiPanel, diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index ea090520fdd46..ffb6d6d98f180 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -11,11 +11,7 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { RolesAPIClient } from '../roles'; -import { RoleMappingsAPIClient } from './role_mappings_api_client'; import { DocumentationLinksService } from './documentation_links'; -import { RoleMappingsGridPage } from './role_mappings_grid'; -import { EditRoleMappingPage } from './edit_role_mapping'; interface CreateParams { getStartServices: StartServicesAccessor<PluginStartDependencies>; @@ -31,7 +27,6 @@ export const roleMappingsManagementApp = Object.freeze({ defaultMessage: 'Role Mappings', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); const roleMappingsBreadcrumbs = [ { text: i18n.translate('xpack.security.roleMapping.breadcrumb', { @@ -41,6 +36,20 @@ export const roleMappingsManagementApp = Object.freeze({ }, ]; + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { RoleMappingsGridPage }, + { EditRoleMappingPage }, + { RoleMappingsAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./role_mappings_grid'), + import('./edit_role_mapping'), + import('./role_mappings_api_client'), + import('../roles'), + ]); + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); const dockLinksService = new DocumentationLinksService(docLinks); const RoleMappingsGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss deleted file mode 100644 index 5256c79f01f10..0000000000000 --- a/x-pack/plugins/security/public/management/roles/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss deleted file mode 100644 index 0153b1734ceba..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './collapsible_panel/index'; -@import './spaces_popover_list/index'; -@import './privileges/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss deleted file mode 100644 index c0f4f8ab9a870..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './collapsible_panel'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 01af7cb4509f6..eb1417600e19b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './collapsible_panel.scss'; + import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss deleted file mode 100644 index a1a9d038065e6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './privilege_feature_icon'; -@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss deleted file mode 100644 index 19547c0e1953e..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './feature_table/index'; -@import './space_aware_privilege_section/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss deleted file mode 100644 index 6a96553742819..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_all_privileges'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx index 2083778e53998..5d7b13acf79da 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './change_all_privileges.scss'; + import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx index 9e4a3a8a99b56..77445952f3d69 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './feature_table_cell.scss'; + import React from 'react'; import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui'; import { SecuredFeature } from '../../../../model'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss deleted file mode 100644 index 3f40f21e102a1..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './privilege_matrix'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss deleted file mode 100644 index 8f47727fdf8d6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 1. Allow table to scroll both directions - */ - -.secPrivilegeMatrix__modal, -.secPrivilegeMatrix__modal .euiModal__flex { - overflow: hidden; /* 1 */ -} - -.secPrivilegeMatrix__row--isBasePrivilege, -.secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace { - background-color: $euiColorLightestShade; -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss new file mode 100644 index 0000000000000..8e2a3b0512afb --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss @@ -0,0 +1,3 @@ +.secPrivilegeTable__row--isGlobalSpace { + background-color: $euiColorLightestShade; +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index ccb5398a11b23..30a275876fdc7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './privilege_space_table.scss'; + import { EuiBadge, EuiBadgeProps, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss deleted file mode 100644 index b40a32cb8df96..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './spaces_popover_list'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index 92e42ec811afc..63ee311f3155e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './spaces_popover_list.scss'; + import { EuiButtonEmpty, EuiContextMenuItem, diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index e1a10fdc2b8c3..9aaa3b47f3b19 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -12,13 +12,7 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; -import { UserAPIClient } from '../users'; -import { RolesAPIClient } from './roles_api_client'; -import { RolesGridPage } from './roles_grid'; -import { EditRolePage } from './edit_role'; import { DocumentationLinksService } from './documentation_links'; -import { IndicesAPIClient } from './indices_api_client'; -import { PrivilegesAPIClient } from './privileges_api_client'; interface CreateParams { fatalErrors: FatalErrorsSetup; @@ -34,11 +28,6 @@ export const rolesManagementApp = Object.freeze({ order: 20, title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), async mount({ basePath, element, setBreadcrumbs }) { - const [ - { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, - { data, features }, - ] = await getStartServices(); - const rolesBreadcrumbs = [ { text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), @@ -46,6 +35,27 @@ export const rolesManagementApp = Object.freeze({ }, ]; + const [ + [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data, features }, + ], + { RolesGridPage }, + { EditRolePage }, + { RolesAPIClient }, + { IndicesAPIClient }, + { PrivilegesAPIClient }, + { UserAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./roles_grid'), + import('./edit_role'), + import('./roles_api_client'), + import('./indices_api_client'), + import('./privileges_api_client'), + import('../users'), + ]); + const rolesAPIClient = new RolesAPIClient(http); const RolesGridPageWithBreadcrumbs = () => { setBreadcrumbs(rolesBreadcrumbs); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss deleted file mode 100644 index 35df0c1b96583..0000000000000 --- a/x-pack/plugins/security/public/management/users/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user/index'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss deleted file mode 100644 index 7b24b74aceba0..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.secUsersEditPage__content { - max-width: $secFormWidth; - margin-left: auto; - margin-right: auto; - flex-grow: 0; -} diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss deleted file mode 100644 index 734ba7882ba72..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss new file mode 100644 index 0000000000000..727fac4782752 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss @@ -0,0 +1,6 @@ +.secUsersEditPage__content { + max-width: 460px; + margin-left: auto; + margin-right: auto; + flex-grow: 0; +} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 6417ce81b647d..1c8130029bb50 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './edit_user_page.scss'; + import { get } from 'lodash'; import React, { Component, Fragment, ChangeEvent } from 'react'; import { diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 82a2b8d2a98ad..9d337c1508ad4 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -12,10 +12,6 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; -import { RolesAPIClient } from '../roles'; -import { UserAPIClient } from './user_api_client'; -import { UsersGridPage } from './users_grid'; -import { EditUserPage } from './edit_user'; interface CreateParams { authc: AuthenticationServiceSetup; @@ -30,7 +26,6 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); const usersBreadcrumbs = [ { text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), @@ -38,6 +33,20 @@ export const usersManagementApp = Object.freeze({ }, ]; + const [ + [{ http, notifications, i18n: i18nStart }], + { UsersGridPage }, + { EditUserPage }, + { UserAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./users_grid'), + import('./edit_user'), + import('./user_api_client'), + import('../roles'), + ]); + const userAPIClient = new UserAPIClient(http); const rolesAPIClient = new RolesAPIClient(http); const UsersGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index eca3e7d6727df..11aadcff377ef 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -74,6 +74,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }; let notifications: ReturnType<typeof coreMock.createSetup>['notifications']; let http: ReturnType<typeof coreMock.createSetup>['http']; @@ -192,6 +193,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: null, lifespanExpiration: now + 2 * 60 * 1000, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.start(); @@ -225,6 +227,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: null, lifespanExpiration: now + 2 * 60 * 1000, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.start(); @@ -251,6 +254,7 @@ describe('Session Timeout', () => { now: now + elapsed, idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.extend('/foo'); expect(http.fetch).toHaveBeenCalledTimes(3); @@ -303,6 +307,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 64 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.start(); expect(http.fetch).toHaveBeenCalled(); @@ -336,6 +341,7 @@ describe('Session Timeout', () => { now: now + elapsed, idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.extend('/foo'); @@ -358,6 +364,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 4 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.start(); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index bd6dbad7dbf14..b06d8fffd4b62 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -6,10 +6,10 @@ import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; import { BroadcastChannel } from 'broadcast-channel'; +import { SessionInfo } from '../../common/types'; import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; -import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -127,7 +127,7 @@ export class SessionTimeout implements ISessionTimeout { this.sessionInfo = sessionInfo; // save the provider name in session storage, we will need it when we log out const key = `${this.tenant}/session_provider`; - sessionStorage.setItem(key, sessionInfo.provider); + sessionStorage.setItem(key, sessionInfo.provider.name); const { timeout, isLifespanTimeout } = this.getTimeout(); if (timeout == null) { diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts deleted file mode 100644 index e9c4b6e281cf3..0000000000000 --- a/x-pack/plugins/security/public/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export interface SessionInfo { - now: number; - idleTimeoutExpiration: number | null; - lifespanExpiration: number | null; - provider: string; -} diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/audit_logger.test.ts index f7ee210a21a74..4dfd69a2ccb1f 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.test.ts @@ -62,7 +62,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => { }); describe(`#savedObjectsAuthorizationSuccess`, () => { - test('logs via auditLogger when xpack.security.audit.enabled is true', () => { + test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); const username = 'foo-user'; @@ -92,3 +92,21 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { ); }); }); + +describe(`#accessAgreementAcknowledged`, () => { + test('logs via auditLogger', () => { + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); + const username = 'foo-user'; + const provider = { type: 'saml', name: 'saml1' }; + + securityAuditLogger.accessAgreementAcknowledged(username, provider); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + 'access_agreement_acknowledged', + 'foo-user acknowledged access agreement (saml/saml1).', + { username, provider } + ); + }); +}); diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/audit_logger.ts index 40b525b5d2188..d7243ecbe13f8 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AuthenticationProvider } from '../../common/types'; import { LegacyAPI } from '../plugin'; export class SecurityAuditLogger { @@ -57,4 +58,12 @@ export class SecurityAuditLogger { } ); } + + accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) { + this.getAuditLogger().log( + 'access_agreement_acknowledged', + `${username} acknowledged access agreement (${provider.type}/${provider.name}).`, + { username, provider } + ); + } } diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts index c14b98ed4781e..888aa3361faf0 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -11,6 +11,7 @@ export const securityAuditLoggerMock = { return ({ savedObjectsAuthorizationFailure: jest.fn(), savedObjectsAuthorizationSuccess: jest.fn(), + accessAgreementAcknowledged: jest.fn(), } as unknown) as jest.Mocked<SecurityAuditLogger>; }, }; diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index a595b63faaf9b..49b7b40659cfc 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -20,7 +20,10 @@ import { elasticsearchServiceMock, sessionStorageMock, } from '../../../../../src/core/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { securityAuditLoggerMock } from '../audit/index.mock'; +import { SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigSchema, createConfig } from '../config'; import { AuthenticationResult } from './authentication_result'; import { Authenticator, AuthenticatorOptions, ProviderSession } from './authenticator'; @@ -39,8 +42,11 @@ function getMockOptions({ selector?: AuthenticatorOptions['config']['authc']['selector']; } = {}) { return { + auditLogger: securityAuditLoggerMock.create(), + getCurrentUser: jest.fn(), clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, + license: licenseMock.create(), loggers: loggingServiceMock.create(), config: createConfig( ConfigSchema.validate({ session, authc: { selector, providers, http } }), @@ -1108,6 +1114,141 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); }); }); + + describe('with Access Agreement', () => { + const mockUser = mockAuthenticatedUser(); + beforeEach(() => { + mockOptions = getMockOptions({ + providers: { + basic: { basic1: { order: 0, accessAgreement: { message: 'some notice' } } }, + }, + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(mockUser) + ); + + authenticator = new Authenticator(mockOptions); + }); + + it('does not redirect to Access Agreement if there is no active session', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect AJAX requests to Access Agreement', async () => { + const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if request cannot be handled', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.notHandled() + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + }); + + it('does not redirect to Access Agreement if authentication fails', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + const failureReason = new Error('something went wrong'); + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.failed(failureReason) + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); + }); + + it('does not redirect to Access Agreement if redirect is required to complete authentication', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.redirectTo('/some-url') + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo('/some-url') + ); + }); + + it('does not redirect to Access Agreement if user has already acknowledged it', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, + accessAgreementAcknowledged: true, + }); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement its own requests', async () => { + const request = httpServerMock.createKibanaRequest({ path: '/security/access_agreement' }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if it is not configured', async () => { + mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + authenticator = new Authenticator(mockOptions); + + const request = httpServerMock.createKibanaRequest(); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if license doesnt allow it.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('redirects to Access Agreement when needed.', async () => { + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + const request = httpServerMock.createKibanaRequest(); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/security/access_agreement?next=%2Fmock-server-basepath%2Fpath' + ) + ); + }); + }); }); describe('`logout` method', () => { @@ -1228,13 +1369,13 @@ describe('Authenticator', () => { now: currentDate, idleTimeoutExpiration: currentDate + 60000, lifespanExpiration: currentDate + 120000, - provider: 'basic1', + provider: { type: 'basic' as 'basic', name: 'basic1' }, }; mockSessionStorage.get.mockResolvedValue({ idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, lifespanExpiration: mockInfo.lifespanExpiration, state, - provider: { type: 'basic', name: mockInfo.provider }, + provider: mockInfo.provider, path: mockOptions.basePath.serverBasePath, }); jest.spyOn(Date, 'now').mockImplementation(() => currentDate); @@ -1274,4 +1415,84 @@ describe('Authenticator', () => { expect(authenticator.isProviderTypeEnabled('saml')).toBe(true); }); }); + + describe('`acknowledgeAccessAgreement` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType<typeof getMockOptions>; + let mockSessionStorage: jest.Mocked<SessionStorage<ProviderSession>>; + let mockSessionValue: any; + beforeEach(() => { + mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessionValue = { + idleTimeoutExpiration: null, + lifespanExpiration: null, + state: { authorization: 'Basic xxx' }, + provider: { type: 'basic', name: 'basic1' }, + path: mockOptions.basePath.serverBasePath, + }; + mockSessionStorage.get.mockResolvedValue(mockSessionValue); + mockOptions.getCurrentUser.mockReturnValue(mockAuthenticatedUser()); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + authenticator = new Authenticator(mockOptions); + }); + + it('fails if user is not authenticated', async () => { + mockOptions.getCurrentUser.mockReturnValue(null); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot acknowledge access agreement for unauthenticated user."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('fails if cannot retrieve user session', async () => { + mockSessionStorage.get.mockResolvedValue(null); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot acknowledge access agreement for unauthenticated user."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('fails if license doesn allow access agreement acknowledgement', async () => { + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Current license does not allow access agreement acknowledgement."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('properly acknowledges access agreement for the authenticated user', async () => { + await authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessionValue, + accessAgreementAcknowledged: true, + }); + + expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith('user', { + type: 'basic', + name: 'basic1', + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index caf5b485d05e3..58dea2b23e546 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -14,6 +14,10 @@ import { HttpServiceSetup, IClusterClient, } from '../../../../../src/core/server'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticatedUser } from '../../common/model'; +import { AuthenticationProvider, SessionInfo } from '../../common/types'; +import { SecurityAuditLogger } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; @@ -32,7 +36,6 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; -import { SessionInfo } from '../../public'; import { canRedirectRequest } from './can_redirect_request'; import { HTTPAuthorizationHeader } from './http_authentication'; @@ -43,7 +46,7 @@ export interface ProviderSession { /** * Name and type of the provider this session belongs to. */ - provider: { type: string; name: string }; + provider: AuthenticationProvider; /** * The Unix time in ms when the session should be considered expired. If `null`, session will stay @@ -67,6 +70,11 @@ export interface ProviderSession { * Cookie "Path" attribute that is validated against the current Kibana server configuration. */ path: string; + + /** + * Indicates whether user acknowledged access agreement or not. + */ + accessAgreementAcknowledged?: boolean; } /** @@ -76,7 +84,7 @@ export interface ProviderLoginAttempt { /** * Name or type of the provider this login attempt is targeted for. */ - provider: { name: string } | { type: string }; + provider: Pick<AuthenticationProvider, 'name'> | Pick<AuthenticationProvider, 'type'>; /** * Login attempt can have any form and defined by the specific provider. @@ -85,8 +93,11 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { + auditLogger: SecurityAuditLogger; + getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick<ConfigType, 'session' | 'authc'>; basePath: HttpServiceSetup['basePath']; + license: SecurityLicense; loggers: LoggerFactory; clusterClient: IClusterClient; sessionStorageFactory: SessionStorageFactory<ProviderSession>; @@ -109,6 +120,11 @@ const providerMap = new Map< [PKIAuthenticationProvider.type, PKIAuthenticationProvider], ]); +/** + * The route to the access agreement UI. + */ +const ACCESS_AGREEMENT_ROUTE = '/security/access_agreement'; + function assertRequest(request: KibanaRequest) { if (!(request instanceof KibanaRequest)) { throw new Error(`Request should be a valid "KibanaRequest" instance, was [${typeof request}].`); @@ -135,7 +151,7 @@ function isLoginAttemptWithProviderName( function isLoginAttemptWithProviderType( attempt: unknown -): attempt is { value: unknown; provider: { type: string } } { +): attempt is { value: unknown; provider: Pick<AuthenticationProvider, 'type'> } { return ( typeof attempt === 'object' && (attempt as any)?.provider?.type && @@ -341,14 +357,7 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const existingSession = await this.getSessionValue(sessionStorage); - // If request doesn't have any session information, isn't attributed with HTTP Authorization - // header and Login Selector is enabled, we must redirect user to the login selector. - const useLoginSelector = - !existingSession && - this.options.config.authc.selector.enabled && - canRedirectRequest(request) && - HTTPAuthorizationHeader.parseFromRequest(request) == null; - if (useLoginSelector) { + if (this.shouldRedirectToLoginSelector(request, existingSession)) { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( @@ -368,7 +377,7 @@ export class Authenticator { ownsSession ? existingSession!.state : null ); - this.updateSessionValue(sessionStorage, { + const updatedSession = this.updateSessionValue(sessionStorage, { provider: { type: provider.type, name: providerName }, isSystemRequest: request.isSystemRequest, authenticationResult, @@ -376,6 +385,20 @@ export class Authenticator { }); if (!authenticationResult.notHandled()) { + if ( + authenticationResult.succeeded() && + this.shouldRedirectToAccessAgreement(request, updatedSession) + ) { + this.logger.debug('Redirecting user to the access agreement screen.'); + return AuthenticationResult.redirectTo( + `${ + this.options.basePath.serverBasePath + }${ACCESS_AGREEMENT_ROUTE}?next=${encodeURIComponent( + `${this.options.basePath.get(request)}${request.url.path}` + )}` + ); + } + return authenticationResult; } } @@ -441,7 +464,7 @@ export class Authenticator { now: Date.now(), idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, lifespanExpiration: sessionValue.lifespanExpiration, - provider: sessionValue.provider.name, + provider: sessionValue.provider, }; } return null; @@ -455,6 +478,32 @@ export class Authenticator { return [...this.providers.values()].some(provider => provider.type === providerType); } + /** + * Acknowledges access agreement on behalf of the currently authenticated user. + * @param request Request instance. + */ + async acknowledgeAccessAgreement(request: KibanaRequest) { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const existingSession = await this.getSessionValue(sessionStorage); + const currentUser = this.options.getCurrentUser(request); + if (!existingSession || !currentUser) { + throw new Error('Cannot acknowledge access agreement for unauthenticated user.'); + } + + if (!this.options.license.getFeatures().allowAccessAgreement) { + throw new Error('Current license does not allow access agreement acknowledgement.'); + } + + sessionStorage.set({ ...existingSession, accessAgreementAcknowledged: true }); + + this.options.auditLogger.accessAgreementAcknowledged( + currentUser.username, + existingSession.provider + ); + } + /** * Initializes HTTP Authentication provider and appends it to the end of the list of enabled * authentication providers. @@ -538,14 +587,14 @@ export class Authenticator { existingSession, isSystemRequest, }: { - provider: { type: string; name: string }; + provider: AuthenticationProvider; authenticationResult: AuthenticationResult; existingSession: ProviderSession | null; isSystemRequest: boolean; } ) { if (!existingSession && !authenticationResult.shouldUpdateState()) { - return; + return null; } // If authentication succeeds or requires redirect we should automatically extend existing user session, @@ -563,9 +612,12 @@ export class Authenticator { (authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401) ) { sessionStorage.clear(); - } else if (sessionCanBeUpdated) { + return null; + } + + if (sessionCanBeUpdated) { const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); - sessionStorage.set({ + const updatedSession = { state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, @@ -573,8 +625,13 @@ export class Authenticator { idleTimeoutExpiration, lifespanExpiration, path: this.serverBasePath, - }); + accessAgreementAcknowledged: existingSession?.accessAgreementAcknowledged, + }; + sessionStorage.set(updatedSession); + return updatedSession; } + + return existingSession; } private getProviderName(query: any): string | null { @@ -600,4 +657,48 @@ export class Authenticator { return { idleTimeoutExpiration, lifespanExpiration }; } + + /** + * Checks whether request should be redirected to the Login Selector UI. + * @param request Request instance. + * @param session Current session value if any. + */ + private shouldRedirectToLoginSelector(request: KibanaRequest, session: ProviderSession | null) { + // Request should be redirected to Login Selector UI only if all following conditions are met: + // 1. Request can be redirected (not API call) + // 2. Request is not authenticated yet + // 3. Login Selector UI is enabled + // 4. Request isn't attributed with HTTP Authorization header + return ( + canRedirectRequest(request) && + !session && + this.options.config.authc.selector.enabled && + HTTPAuthorizationHeader.parseFromRequest(request) == null + ); + } + + /** + * Checks whether request should be redirected to the Access Agreement UI. + * @param request Request instance. + * @param session Current session value if any. + */ + private shouldRedirectToAccessAgreement(request: KibanaRequest, session: ProviderSession | null) { + // Request should be redirected to Access Agreement UI only if all following conditions are met: + // 1. Request can be redirected (not API call) + // 2. Request is authenticated, but user hasn't acknowledged access agreement in the current + // session yet (based on the flag we store in the session) + // 3. Request is authenticated by the provider that has `accessAgreement` configured + // 4. Current license allows access agreement + // 5. And it's not a request to the Access Agreement UI itself + return ( + canRedirectRequest(request) && + session != null && + !session.accessAgreementAcknowledged && + (this.options.config.authc.providers as Record<string, any>)[session.provider.type]?.[ + session.provider.name + ]?.accessAgreement && + this.options.license.getFeatures().allowAccessAgreement && + request.url.pathname !== ACCESS_AGREEMENT_ROUTE + ); + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 9397a7a42b326..7cd3ac18634f7 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 6609f8707976b..1c1e0ed781f18 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -19,6 +19,7 @@ import { elasticsearchServiceMock, } from '../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { securityAuditLoggerMock } from '../audit/index.mock'; import { AuthenticationHandler, @@ -40,9 +41,11 @@ import { InvalidateAPIKeyParams, } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; +import { SecurityAuditLogger } from '../audit'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { + auditLogger: jest.Mocked<SecurityAuditLogger>; config: ConfigType; loggers: LoggerFactory; http: jest.Mocked<CoreSetup['http']>; @@ -52,6 +55,7 @@ describe('setupAuthentication()', () => { let mockScopedClusterClient: jest.Mocked<PublicMethodsOf<ScopedClusterClient>>; beforeEach(() => { mockSetupAuthenticationParams = { + auditLogger: securityAuditLoggerMock.create(), http: coreMock.createSetup().http, config: createConfig( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index d76a5a533d498..779b852195b02 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -10,12 +10,13 @@ import { KibanaRequest, LoggerFactory, } from '../../../../../src/core/server'; +import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; +import { SecurityAuditLogger } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; -import { SecurityLicense } from '../../common/licensing'; export { canRedirectRequest } from './can_redirect_request'; export { Authenticator, ProviderLoginAttempt } from './authenticator'; @@ -35,6 +36,7 @@ export { } from './http_authentication'; interface SetupAuthenticationParams { + auditLogger: SecurityAuditLogger; http: CoreSetup['http']; clusterClient: IClusterClient; config: ConfigType; @@ -45,6 +47,7 @@ interface SetupAuthenticationParams { export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>; export async function setupAuthentication({ + auditLogger, http, clusterClient, config, @@ -82,9 +85,12 @@ export async function setupAuthentication({ }; const authenticator = new Authenticator({ + auditLogger, + getCurrentUser, clusterClient, basePath: http.basePath, config: { session: config.session, authc: config.authc }, + license, loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, @@ -171,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 46a7ee79ee60c..2c24864649977 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -27,8 +27,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -69,8 +72,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -111,8 +117,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -361,20 +370,6 @@ describe('config schema', () => { `); }); - it('does not allow custom description', () => { - expect(() => - ConfigSchema.validate({ - authc: { - providers: { basic: { basic1: { order: 0, description: 'Some description' } } }, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.description]: \`basic\` provider does not support custom description." -`); - }); - it('cannot be hidden from selector', () => { expect(() => ConfigSchema.validate({ @@ -410,7 +405,9 @@ describe('config schema', () => { Object { "basic": Object { "basic1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, @@ -433,20 +430,6 @@ describe('config schema', () => { `); }); - it('does not allow custom description', () => { - expect(() => - ConfigSchema.validate({ - authc: { - providers: { token: { token1: { order: 0, description: 'Some description' } } }, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.description]: \`token\` provider does not support custom description." -`); - }); - it('cannot be hidden from selector', () => { expect(() => ConfigSchema.validate({ @@ -482,7 +465,9 @@ describe('config schema', () => { Object { "token": Object { "token1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, @@ -759,12 +744,16 @@ describe('config schema', () => { Object { "basic": Object { "basic1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, "basic2": Object { + "description": "Log in with Elasticsearch", "enabled": false, + "icon": "logoElastic", "order": 1, "showInSelector": true, }, @@ -911,20 +900,12 @@ describe('createConfig()', () => { "sortedProviders": Array [ Object { "name": "saml", - "options": Object { - "description": undefined, - "order": 0, - "showInSelector": true, - }, + "order": 0, "type": "saml", }, Object { "name": "basic", - "options": Object { - "description": undefined, - "order": 1, - "showInSelector": true, - }, + "order": 1, "type": "basic", }, ], @@ -1015,47 +996,27 @@ describe('createConfig()', () => { Array [ Object { "name": "oidc1", - "options": Object { - "description": undefined, - "order": 0, - "showInSelector": true, - }, + "order": 0, "type": "oidc", }, Object { "name": "saml2", - "options": Object { - "description": undefined, - "order": 1, - "showInSelector": true, - }, + "order": 1, "type": "saml", }, Object { "name": "saml1", - "options": Object { - "description": undefined, - "order": 2, - "showInSelector": true, - }, + "order": 2, "type": "saml", }, Object { "name": "basic1", - "options": Object { - "description": undefined, - "order": 3, - "showInSelector": true, - }, + "order": 3, "type": "basic", }, Object { "name": "oidc2", - "options": Object { - "description": undefined, - "order": 4, - "showInSelector": true, - }, + "order": 4, "type": "oidc", }, ] diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 97ff7d00a4336..8fe79a788ac51 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -6,6 +6,7 @@ import crypto from 'crypto'; import { schema, Type, TypeOf } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { Logger } from '../../../../src/core/server'; export type ConfigType = ReturnType<typeof createConfig>; @@ -21,7 +22,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type<any>) = ); type ProvidersCommonConfigType = Record< - 'enabled' | 'showInSelector' | 'order' | 'description', + 'enabled' | 'showInSelector' | 'order' | 'description' | 'hint' | 'icon', Type<any> >; function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonConfigType> = {}) { @@ -30,6 +31,9 @@ function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonCon showInSelector: schema.boolean({ defaultValue: true }), order: schema.number({ min: 0 }), description: schema.maybe(schema.string()), + hint: schema.maybe(schema.string()), + icon: schema.maybe(schema.string()), + accessAgreement: schema.maybe(schema.object({ message: schema.string() })), ...overrides, }; } @@ -53,11 +57,12 @@ type ProvidersConfigType = TypeOf<typeof providersConfigSchema>; const providersConfigSchema = schema.object( { basic: getUniqueProviderSchema('basic', { - description: schema.maybe( - schema.any({ - validate: () => '`basic` provider does not support custom description.', - }) - ), + description: schema.string({ + defaultValue: i18n.translate('xpack.security.loginWithElasticsearchLabel', { + defaultMessage: 'Log in with Elasticsearch', + }), + }), + icon: schema.string({ defaultValue: 'logoElastic' }), showInSelector: schema.boolean({ defaultValue: true, validate: value => { @@ -68,11 +73,12 @@ const providersConfigSchema = schema.object( }), }), token: getUniqueProviderSchema('token', { - description: schema.maybe( - schema.any({ - validate: () => '`token` provider does not support custom description.', - }) - ), + description: schema.string({ + defaultValue: i18n.translate('xpack.security.loginWithElasticsearchLabel', { + defaultMessage: 'Log in with Elasticsearch', + }), + }), + icon: schema.string({ defaultValue: 'logoElastic' }), showInSelector: schema.boolean({ defaultValue: true, validate: value => { @@ -131,6 +137,7 @@ const providersConfigSchema = schema.object( export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), loginAssistanceMessage: schema.string({ defaultValue: '' }), + loginHelp: schema.maybe(schema.string()), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), @@ -147,7 +154,17 @@ export const ConfigSchema = schema.object({ selector: schema.object({ enabled: schema.maybe(schema.boolean()) }), providers: schema.oneOf([schema.arrayOf(schema.string()), providersConfigSchema], { defaultValue: { - basic: { basic: { enabled: true, showInSelector: true, order: 0, description: undefined } }, + basic: { + basic: { + enabled: true, + showInSelector: true, + order: 0, + description: undefined, + hint: undefined, + icon: undefined, + accessAgreement: undefined, + }, + }, token: undefined, saml: undefined, oidc: undefined, @@ -225,25 +242,19 @@ export function createConfig( const sortedProviders: Array<{ type: keyof ProvidersConfigType; name: string; - options: { order: number; showInSelector: boolean; description?: string }; + order: number; }> = []; for (const [type, providerGroup] of Object.entries(providers)) { - for (const [name, { enabled, showInSelector, order, description }] of Object.entries( - providerGroup ?? {} - )) { + for (const [name, { enabled, order }] of Object.entries(providerGroup ?? {})) { if (!enabled) { delete providerGroup![name]; } else { - sortedProviders.push({ - type: type as any, - name, - options: { order, showInSelector, description }, - }); + sortedProviders.push({ type: type as any, name, order }); } } } - sortedProviders.sort(({ options: { order: orderA } }, { options: { order: orderB } }) => + sortedProviders.sort(({ order: orderA }, { order: orderB }) => orderA < orderB ? -1 : orderA > orderB ? 1 : 0 ); @@ -253,7 +264,8 @@ export function createConfig( typeof config.authc.selector.enabled === 'boolean' ? config.authc.selector.enabled : !isUsingLegacyProvidersFormat && - sortedProviders.filter(provider => provider.options.showInSelector).length > 1; + sortedProviders.filter(({ type, name }) => providers[type]?.[name].showInSelector).length > + 1; return { ...config, diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index ababf12c2be60..a6407366bbd3b 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -8,6 +8,7 @@ import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; +import { licenseMock } from '../common/licensing/index.mock'; function createSetupMock() { const mockAuthz = authorizationMock.create(); @@ -19,6 +20,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), + license: licenseMock.create(), __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 3ce0198273af9..d58c999ddccdf 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 9dd4aaafa3494..97f5aea888dc7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -48,8 +48,18 @@ export interface LegacyAPI { * Describes public Security plugin contract returned at the `setup` stage. */ export interface SecurityPluginSetup { - authc: Authentication; + authc: Pick< + Authentication, + | 'isAuthenticated' + | 'getCurrentUser' + | 'areAPIKeysEnabled' + | 'createAPIKey' + | 'invalidateAPIKey' + | 'grantAPIKeyAsInternalUser' + | 'invalidateAPIKeyAsInternalUser' + >; authz: Pick<Authorization, 'actions' | 'checkPrivilegesWithRequest' | 'mode'>; + license: SecurityLicense; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -64,7 +74,6 @@ export interface SecurityPluginSetup { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void; registerPrivilegesWithCluster: () => void; - license: SecurityLicense; }; } @@ -126,7 +135,9 @@ export class Plugin { license$: licensing.license$, }); + const auditLogger = new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger); const authc = await setupAuthentication({ + auditLogger, http: core.http, clusterClient: this.clusterClient, config, @@ -146,7 +157,7 @@ export class Plugin { }); setupSavedObjects({ - auditLogger: new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger), + auditLogger, authz, savedObjects: core.savedObjects, getSpacesService: this.getSpacesService, @@ -167,7 +178,15 @@ export class Plugin { }); return deepFreeze<SecurityPluginSetup>({ - authc, + authc: { + isAuthenticated: authc.isAuthenticated, + getCurrentUser: authc.getCurrentUser, + areAPIKeysEnabled: authc.areAPIKeysEnabled, + createAPIKey: authc.createAPIKey, + invalidateAPIKey: authc.invalidateAPIKey, + grantAPIKeyAsInternalUser: authc.grantAPIKeyAsInternalUser, + invalidateAPIKeyAsInternalUser: authc.invalidateAPIKeyAsInternalUser, + }, authz: { actions: authz.actions, @@ -175,6 +194,8 @@ export class Plugin { mode: authz.mode, }, + license, + registerSpacesService: service => { if (this.wasSpacesServiceAccessed()) { throw new Error('Spaces service has been accessed before registration.'); @@ -187,8 +208,6 @@ export class Plugin { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI), registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), - - license, }, }); } diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 156c03e90210b..5a0401e6320b4 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -12,6 +12,7 @@ import { RequestHandlerContext, RouteConfig, } from '../../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { Authentication, AuthenticationResult, @@ -28,11 +29,13 @@ import { routeDefinitionParamsMock } from '../index.mock'; describe('Common authentication routes', () => { let router: jest.Mocked<IRouter>; let authc: jest.Mocked<Authentication>; + let license: jest.Mocked<SecurityLicense>; let mockContext: RequestHandlerContext; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; authc = routeParamsMock.authc; + license = routeParamsMock.license; mockContext = ({ licensing: { @@ -433,4 +436,61 @@ describe('Common authentication routes', () => { }); }); }); + + describe('acknowledge access agreement', () => { + let routeHandler: RequestHandler<any, any, any>; + let routeConfig: RouteConfig<any, any, any, any>; + beforeEach(() => { + const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/access_agreement/acknowledge' + )!; + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + routeConfig = acsRouteConfig; + routeHandler = acsRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it(`returns 403 if current license doesn't allow access agreement acknowledgement.`, async () => { + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 403, + payload: { message: `Current license doesn't support access agreement.` }, + options: { body: { message: `Current license doesn't support access agreement.` } }, + }); + }); + + it('returns 500 if acknowledge throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + payload: 'Internal Error', + options: {}, + }); + }); + + it('returns 204 if successfully acknowledged.', async () => { + authc.acknowledgeAccessAgreement.mockResolvedValue(undefined); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 204, + options: {}, + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index abab67c9cd1d2..91783140539a5 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -18,7 +18,13 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes that are common to various authentication mechanisms. */ -export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDefinitionParams) { +export function defineCommonRoutes({ + router, + authc, + basePath, + license, + logger, +}: RouteDefinitionParams) { // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. for (const path of ['/api/security/logout', '/api/security/v1/logout']) { router.get( @@ -135,4 +141,26 @@ export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDef } }) ); + + router.post( + { path: '/internal/security/access_agreement/acknowledge', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + // If license doesn't allow access agreement we shouldn't handle request. + if (!license.getFeatures().allowAccessAgreement) { + logger.warn(`Attempted to acknowledge access agreement when license doesn't allow it.`); + return response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }); + } + + try { + await authc.acknowledgeAccessAgreement(request); + } catch (err) { + logger.error(err); + return response.internalError(); + } + + return response.noContent(); + }) + ); } diff --git a/x-pack/plugins/security/server/routes/licensed_route_handler.ts b/x-pack/plugins/security/server/routes/licensed_route_handler.ts index b113b2ca59e3e..d8c212aa2d217 100644 --- a/x-pack/plugins/security/server/routes/licensed_route_handler.ts +++ b/x-pack/plugins/security/server/routes/licensed_route_handler.ts @@ -4,10 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { KibanaResponseFactory, RequestHandler, RouteMethod } from 'kibana/server'; -export const createLicensedRouteHandler = <P, Q, B>(handler: RequestHandler<P, Q, B>) => { - const licensedRouteHandler: RequestHandler<P, Q, B> = (context, request, responseToolkit) => { +export const createLicensedRouteHandler = < + P, + Q, + B, + M extends RouteMethod, + R extends KibanaResponseFactory +>( + handler: RequestHandler<P, Q, B, M, R> +) => { + const licensedRouteHandler: RequestHandler<P, Q, B, M, R> = ( + context, + request, + responseToolkit + ) => { const { license } = context.licensing; const licenseCheck = license.check('security', 'basic'); if (licenseCheck.state === 'unavailable' || licenseCheck.state === 'invalid') { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index fd05821f9d520..c163ff4e256cd 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -53,7 +53,7 @@ describe('Change password', () => { now: Date.now(), idleTimeoutExpiration: null, lifespanExpiration: null, - provider: 'basic', + provider: { type: 'basic', name: 'basic' }, }); mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts new file mode 100644 index 0000000000000..3d616575b8413 --- /dev/null +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + RequestHandler, + RouteConfig, + kibanaResponseFactory, + IRouter, + HttpResources, + HttpResourcesRequestHandler, + RequestHandlerContext, +} from '../../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; +import { AuthenticationProvider } from '../../../common/types'; +import { ConfigType } from '../../config'; +import { defineAccessAgreementRoutes } from './access_agreement'; + +import { httpResourcesMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { Authentication } from '../../authentication'; + +describe('Access agreement view routes', () => { + let httpResources: jest.Mocked<HttpResources>; + let router: jest.Mocked<IRouter>; + let config: ConfigType; + let authc: jest.Mocked<Authentication>; + let license: jest.Mocked<SecurityLicense>; + let mockContext: RequestHandlerContext; + beforeEach(() => { + const routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router; + httpResources = routeParamsMock.httpResources; + authc = routeParamsMock.authc; + config = routeParamsMock.config; + license = routeParamsMock.license; + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: 'valid' }) }, + }, + } as unknown) as RequestHandlerContext; + + defineAccessAgreementRoutes(routeParamsMock); + }); + + describe('View route', () => { + let routeHandler: HttpResourcesRequestHandler<any, any, any>; + let routeConfig: RouteConfig<any, any, any, 'get'>; + beforeEach(() => { + const [viewRouteConfig, viewRouteHandler] = httpResources.register.mock.calls.find( + ([{ path }]) => path === '/security/access_agreement' + )!; + + routeConfig = viewRouteConfig; + routeHandler = viewRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('does not render view if current license does not allow access agreement.', async () => { + const request = httpServerMock.createKibanaRequest(); + const responseFactory = httpResourcesMock.createResponseFactory(); + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await routeHandler(mockContext, request, responseFactory); + + expect(responseFactory.renderCoreApp).not.toHaveBeenCalledWith(); + expect(responseFactory.forbidden).toHaveBeenCalledTimes(1); + }); + + it('renders view.', async () => { + const request = httpServerMock.createKibanaRequest(); + const responseFactory = httpResourcesMock.createResponseFactory(); + + await routeHandler(mockContext, request, responseFactory); + + expect(responseFactory.renderCoreApp).toHaveBeenCalledWith(); + }); + }); + + describe('Access agreement state route', () => { + let routeHandler: RequestHandler<any, any, any, 'get'>; + let routeConfig: RouteConfig<any, any, any, 'get'>; + beforeEach(() => { + const [loginStateRouteConfig, loginStateRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/access_agreement/state' + )!; + + routeConfig = loginStateRouteConfig; + routeHandler = loginStateRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('returns `403` if current license does not allow access agreement.', async () => { + const request = httpServerMock.createKibanaRequest(); + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 403, + payload: { message: `Current license doesn't support access agreement.` }, + options: { body: { message: `Current license doesn't support access agreement.` } }, + }); + }); + + it('returns empty `accessAgreement` if session info is not available.', async () => { + const request = httpServerMock.createKibanaRequest(); + + authc.getSessionInfo.mockResolvedValue(null); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + options: { body: { accessAgreement: '' } }, + payload: { accessAgreement: '' }, + status: 200, + }); + }); + + it('returns non-empty `accessAgreement` only if it is configured.', async () => { + const request = httpServerMock.createKibanaRequest(); + + config.authc = routeDefinitionParamsMock.create({ + authc: { + providers: { + basic: { basic1: { order: 0 } }, + saml: { + saml1: { + order: 1, + realm: 'realm1', + accessAgreement: { message: 'Some access agreement' }, + }, + }, + }, + }, + }).config.authc; + + const cases: Array<[AuthenticationProvider, string]> = [ + [{ type: 'basic', name: 'basic1' }, ''], + [{ type: 'saml', name: 'saml1' }, 'Some access agreement'], + [{ type: 'unknown-type', name: 'unknown-name' }, ''], + ]; + + for (const [sessionProvider, expectedAccessAgreement] of cases) { + authc.getSessionInfo.mockResolvedValue({ + now: Date.now(), + idleTimeoutExpiration: null, + lifespanExpiration: null, + provider: sessionProvider, + }); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + options: { body: { accessAgreement: expectedAccessAgreement } }, + payload: { accessAgreement: expectedAccessAgreement }, + status: 200, + }); + } + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts new file mode 100644 index 0000000000000..49e1ff42a28a2 --- /dev/null +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConfigType } from '../../config'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for the Access Agreement view. + */ +export function defineAccessAgreementRoutes({ + authc, + httpResources, + license, + config, + router, + logger, +}: RouteDefinitionParams) { + // If license doesn't allow access agreement we shouldn't handle request. + const canHandleRequest = () => license.getFeatures().allowAccessAgreement; + + httpResources.register( + { path: '/security/access_agreement', validate: false }, + createLicensedRouteHandler(async (context, request, response) => + canHandleRequest() + ? response.renderCoreApp() + : response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }) + ) + ); + + router.get( + { path: '/internal/security/access_agreement/state', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + if (!canHandleRequest()) { + return response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }); + } + + // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is + // authenticated with the help of HTTP authentication), that means we should safely check if + // we have it and can get a corresponding configuration. + try { + const session = await authc.getSessionInfo(request); + const accessAgreement = + (session && + config.authc.providers[ + session.provider.type as keyof ConfigType['authc']['providers'] + ]?.[session.provider.name]?.accessAgreement?.message) || + ''; + + return response.ok({ body: { accessAgreement } }); + } catch (err) { + logger.error(err); + return response.internalError(); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts index a8e7e905b119a..7cddef9bf2b98 100644 --- a/x-pack/plugins/security/server/routes/views/index.test.ts +++ b/x-pack/plugins/security/server/routes/views/index.test.ts @@ -20,15 +20,18 @@ describe('View routes', () => { expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) .toMatchInlineSnapshot(` Array [ + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot( - `Array []` - ); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/access_agreement/state", + ] + `); }); it('registers Login routes if `basic` provider is enabled', () => { @@ -43,6 +46,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -52,6 +56,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); @@ -68,6 +73,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -77,6 +83,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); @@ -93,6 +100,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -102,6 +110,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); diff --git a/x-pack/plugins/security/server/routes/views/index.ts b/x-pack/plugins/security/server/routes/views/index.ts index 255989dfeb90c..b9de58d47fe40 100644 --- a/x-pack/plugins/security/server/routes/views/index.ts +++ b/x-pack/plugins/security/server/routes/views/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineAccessAgreementRoutes } from './access_agreement'; import { defineAccountManagementRoutes } from './account_management'; import { defineLoggedOutRoutes } from './logged_out'; import { defineLoginRoutes } from './login'; @@ -20,6 +21,7 @@ export function defineViewRoutes(params: RouteDefinitionParams) { defineLoginRoutes(params); } + defineAccessAgreementRoutes(params); defineAccountManagementRoutes(params); defineLoggedOutRoutes(params); defineLogoutRoutes(params); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 3ff05d242d9dd..7cb73c49f9cbc 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -39,7 +39,7 @@ describe('LoggedOut view routes', () => { it('redirects user to the root page if they have a session already.', async () => { authc.getSessionInfo.mockResolvedValue({ - provider: 'basic', + provider: { type: 'basic', name: 'basic' }, now: 0, idleTimeoutExpiration: null, lifespanExpiration: null, diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index d43319efbdfb9..014ad390a3d53 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -15,7 +15,7 @@ import { RouteConfig, } from '../../../../../../src/core/server'; import { SecurityLicense } from '../../../common/licensing'; -import { LoginState } from '../../../common/login_state'; +import { LoginSelectorProvider } from '../../../common/login_state'; import { ConfigType } from '../../config'; import { defineLoginRoutes } from './login'; @@ -141,6 +141,10 @@ describe('Login view routes', () => { }); describe('Login state route', () => { + function getAuthcConfig(authcConfig: Record<string, unknown> = {}) { + return routeDefinitionParamsMock.create({ authc: { ...authcConfig } }).config.authc; + } + let routeHandler: RequestHandler<any, any, any, 'get'>; let routeConfig: RouteConfig<any, any, any, 'get'>; beforeEach(() => { @@ -159,6 +163,7 @@ describe('Login view routes', () => { it('returns only required license features.', async () => { license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, allowLogin: true, allowRbac: false, allowRoleDocumentLevelSecurity: true, @@ -176,9 +181,11 @@ describe('Login view routes', () => { const expectedPayload = { allowLogin: true, layout: 'error-es-unavailable', - showLoginForm: true, requiresSecureConnection: false, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + }, }; await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) @@ -198,9 +205,11 @@ describe('Login view routes', () => { const expectedPayload = { allowLogin: true, layout: 'form', - showLoginForm: true, requiresSecureConnection: false, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + }, }; await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) @@ -229,22 +238,46 @@ describe('Login view routes', () => { }); }); - it('returns `showLoginForm: true` only if either `basic` or `token` provider is enabled.', async () => { + it('returns `useLoginForm: true` for `basic` and `token` providers.', async () => { license.getFeatures.mockReturnValue({ allowLogin: true, showLogin: true } as any); const request = httpServerMock.createKibanaRequest(); const contextMock = coreMock.createRequestHandlerContext(); - const cases: Array<[boolean, ConfigType['authc']['sortedProviders']]> = [ - [false, []], - [true, [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }]], - [true, [{ type: 'token', name: 'token1', options: { order: 0, showInSelector: true } }]], + const cases: Array<[LoginSelectorProvider[], ConfigType['authc']]> = [ + [[], getAuthcConfig({ providers: { basic: { basic1: { order: 0, enabled: false } } } })], + [ + [ + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], + getAuthcConfig({ providers: { basic: { basic1: { order: 0 } } } }), + ], + [ + [ + { + name: 'token1', + type: 'token', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], + getAuthcConfig({ providers: { token: { token1: { order: 0 } } } }), + ], ]; - for (const [showLoginForm, sortedProviders] of cases) { - config.authc.sortedProviders = sortedProviders; + for (const [providers, authcConfig] of cases) { + config.authc = authcConfig; - const expectedPayload = expect.objectContaining({ showLoginForm }); + const expectedPayload = expect.objectContaining({ + selector: { enabled: false, providers }, + }); await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) ).resolves.toEqual({ @@ -261,81 +294,142 @@ describe('Login view routes', () => { const request = httpServerMock.createKibanaRequest(); const contextMock = coreMock.createRequestHandlerContext(); - const cases: Array<[ - boolean, - ConfigType['authc']['sortedProviders'], - LoginState['selector']['providers'] - ]> = [ - // selector is disabled, providers shouldn't be returned. + const cases: Array<[ConfigType['authc'], LoginSelectorProvider[]]> = [ + // selector is disabled, multiple providers, but only basic provider should be returned. [ - false, + getAuthcConfig({ + selector: { enabled: false }, + providers: { + basic: { basic1: { order: 0 } }, + saml: { saml1: { order: 1, realm: 'realm1' } }, + }, + }), [ - { type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }, - { type: 'saml', name: 'saml1', options: { order: 1, showInSelector: true } }, + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, ], - [], ], - // selector is enabled, but only basic/token is available, providers shouldn't be returned. + // selector is enabled, but only basic/token is available and should be returned. [ - true, - [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }], - [], + getAuthcConfig({ + selector: { enabled: true }, + providers: { basic: { basic1: { order: 0 } } }, + }), + [ + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], ], - // selector is enabled, non-basic/token providers should be returned + // selector is enabled, all providers should be returned [ - true, + getAuthcConfig({ + selector: { enabled: true }, + providers: { + basic: { + basic1: { + order: 0, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'logoElastic', + }, + }, + saml: { + saml1: { order: 1, description: 'some-desc2', realm: 'realm1', icon: 'some-icon2' }, + saml2: { order: 2, description: 'some-desc3', hint: 'some-hint3', realm: 'realm2' }, + }, + }, + }), [ { type: 'basic', name: 'basic1', - options: { order: 0, showInSelector: true, description: 'some-desc1' }, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'logoElastic', + usesLoginForm: true, }, { type: 'saml', name: 'saml1', - options: { order: 1, showInSelector: true, description: 'some-desc2' }, + description: 'some-desc2', + icon: 'some-icon2', + usesLoginForm: false, }, { type: 'saml', name: 'saml2', - options: { order: 2, showInSelector: true, description: 'some-desc3' }, + description: 'some-desc3', + hint: 'some-hint3', + usesLoginForm: false, }, ], - [ - { type: 'saml', name: 'saml1', description: 'some-desc2' }, - { type: 'saml', name: 'saml2', description: 'some-desc3' }, - ], ], - // selector is enabled, only non-basic/token providers that are enabled in selector should be returned. + // selector is enabled, only providers that are enabled should be returned. [ - true, + getAuthcConfig({ + selector: { enabled: true }, + providers: { + basic: { + basic1: { + order: 0, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'some-icon1', + }, + }, + saml: { + saml1: { + order: 1, + description: 'some-desc2', + realm: 'realm1', + showInSelector: false, + }, + saml2: { + order: 2, + description: 'some-desc3', + hint: 'some-hint3', + icon: 'some-icon3', + realm: 'realm2', + }, + }, + }, + }), [ { type: 'basic', name: 'basic1', - options: { order: 0, showInSelector: true, description: 'some-desc1' }, - }, - { - type: 'saml', - name: 'saml1', - options: { order: 1, showInSelector: false, description: 'some-desc2' }, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'some-icon1', + usesLoginForm: true, }, { type: 'saml', name: 'saml2', - options: { order: 2, showInSelector: true, description: 'some-desc3' }, + description: 'some-desc3', + hint: 'some-hint3', + icon: 'some-icon3', + usesLoginForm: false, }, ], - [{ type: 'saml', name: 'saml2', description: 'some-desc3' }], ], ]; - for (const [selectorEnabled, sortedProviders, expectedProviders] of cases) { - config.authc.selector.enabled = selectorEnabled; - config.authc.sortedProviders = sortedProviders; + for (const [authcConfig, expectedProviders] of cases) { + config.authc = authcConfig; const expectedPayload = expect.objectContaining({ - selector: { enabled: selectorEnabled, providers: expectedProviders }, + selector: { enabled: authcConfig.selector.enabled, providers: expectedProviders }, }); await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 4d6747de713f7..f72facb2e24cc 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -55,15 +55,16 @@ export function defineLoginRoutes({ const { allowLogin, layout = 'form' } = license.getFeatures(); const { sortedProviders, selector } = config.authc; - let showLoginForm = false; const providers = []; - for (const { type, name, options } of sortedProviders) { - if (options.showInSelector) { - if (type === 'basic' || type === 'token') { - showLoginForm = true; - } else if (selector.enabled) { - providers.push({ type, name, description: options.description }); - } + for (const { type, name } of sortedProviders) { + // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can + // be sure that config is present for every provider in `config.authc.sortedProviders`. + const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!; + + // Include provider into the list if either selector is enabled or provider uses login form. + const usesLoginForm = type === 'basic' || type === 'token'; + if (showInSelector && (usesLoginForm || selector.enabled)) { + providers.push({ type, name, usesLoginForm, description, hint, icon }); } } @@ -71,7 +72,7 @@ export function defineLoginRoutes({ allowLogin, layout, requiresSecureConnection: config.secureCookies, - showLoginForm, + loginHelp: config.loginHelp, selector: { enabled: selector.enabled, providers }, }; diff --git a/x-pack/plugins/siem/.gitattributes b/x-pack/plugins/siem/.gitattributes index 96ab5dadbda10..8dc2df600b211 100644 --- a/x-pack/plugins/siem/.gitattributes +++ b/x-pack/plugins/siem/.gitattributes @@ -1,4 +1,6 @@ # Auto-collapse generated files in GitHub # https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github x-pack/plugins/siem/server/graphql/types.ts linguist-generated=true +x-pack/plugins/siem/public/graphql/types.ts linguist-generated=true +x-pack/plugins/siem/public/graphql/introspection.json linguist-generated=true diff --git a/x-pack/plugins/siem/common/constants.ts b/x-pack/plugins/siem/common/constants.ts index edde5c6b8fa0d..bcf5c78be3644 100644 --- a/x-pack/plugins/siem/common/constants.ts +++ b/x-pack/plugins/siem/common/constants.ts @@ -6,6 +6,8 @@ export const APP_ID = 'siem'; export const APP_NAME = 'SIEM'; +export const APP_ICON = 'securityAnalyticsApp'; +export const APP_PATH = `/app/${APP_ID}`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts new file mode 100644 index 0000000000000..55b4f9c6aca4d --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -0,0 +1,280 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; +import { SavedObjectsClient } from 'kibana/server'; + +import { unionWithNullType } from '../../utility_types'; +import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; +import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; + +/* + * ColumnHeader Types + */ +const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ + aggregatable: unionWithNullType(runtimeTypes.boolean), + category: unionWithNullType(runtimeTypes.string), + columnHeaderType: unionWithNullType(runtimeTypes.string), + description: unionWithNullType(runtimeTypes.string), + example: unionWithNullType(runtimeTypes.string), + indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + placeholder: unionWithNullType(runtimeTypes.string), + searchable: unionWithNullType(runtimeTypes.boolean), + type: unionWithNullType(runtimeTypes.string), +}); + +/* + * DataProvider Types + */ +const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ + field: unionWithNullType(runtimeTypes.string), + displayField: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), + displayValue: unionWithNullType(runtimeTypes.string), + operator: unionWithNullType(runtimeTypes.string), +}); + +const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), +}); + +const SavedDataProviderRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), + and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), +}); + +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + +/* + * kqlQuery -> filterQuery Types + */ +const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ + kind: unionWithNullType(runtimeTypes.string), + expression: unionWithNullType(runtimeTypes.string), +}); + +const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), + serializedQuery: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), +}); + +/* + * DatePicker Range Types + */ +const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ + start: unionWithNullType(runtimeTypes.number), + end: unionWithNullType(runtimeTypes.number), +}); + +/* + * Favorite Types + */ +const SavedFavoriteRuntimeType = runtimeTypes.partial({ + keySearch: unionWithNullType(runtimeTypes.string), + favoriteDate: unionWithNullType(runtimeTypes.number), + fullName: unionWithNullType(runtimeTypes.string), + userName: unionWithNullType(runtimeTypes.string), +}); + +/* + * Sort Types + */ +const SavedSortRuntimeType = runtimeTypes.partial({ + columnId: unionWithNullType(runtimeTypes.string), + sortDirection: unionWithNullType(runtimeTypes.string), +}); + +/* + * Timeline Types + */ + +export enum TimelineType { + default = 'default', + template = 'template', +} + +export const TimelineTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineType.template), + runtimeTypes.literal(TimelineType.default), +]); + +export const SavedTimelineRuntimeType = runtimeTypes.partial({ + columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), + dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), + description: unionWithNullType(runtimeTypes.string), + eventType: unionWithNullType(runtimeTypes.string), + favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), + kqlMode: unionWithNullType(runtimeTypes.string), + kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), + title: unionWithNullType(runtimeTypes.string), + templateTimelineId: unionWithNullType(runtimeTypes.string), + templateTimelineVersion: unionWithNullType(runtimeTypes.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), + dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), + sort: unionWithNullType(SavedSortRuntimeType), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), +}); + +export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} + +export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} + +/** + * Timeline Saved object type with metadata + */ + +export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedTimelineRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + savedObjectId: runtimeTypes.string, + }), +]); + +export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ + SavedTimelineRuntimeType, + runtimeTypes.type({ + savedObjectId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + noteIds: runtimeTypes.array(runtimeTypes.string), + notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + pinnedEventIds: runtimeTypes.array(runtimeTypes.string), + pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), + }), +]); + +export interface TimelineSavedObject + extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {} + +/** + * All Timeline Saved object type with metadata + */ +export const TimelineResponseType = runtimeTypes.type({ + data: runtimeTypes.type({ + persistTimeline: runtimeTypes.intersection([ + runtimeTypes.partial({ + code: unionWithNullType(runtimeTypes.number), + message: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.type({ + timeline: TimelineSavedToReturnObjectRuntimeType, + }), + ]), + }), +}); + +export interface TimelineResponse extends runtimeTypes.TypeOf<typeof TimelineResponseType> {} + +/** + * All Timeline Saved object type with metadata + */ + +export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ + total: runtimeTypes.number, + data: TimelineSavedToReturnObjectRuntimeType, +}); + +export interface AllTimelineSavedObject + extends runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType> {} + +/** + * Import/export timelines + */ + +export type ExportTimelineSavedObjectsClient = Pick< + SavedObjectsClient, + | 'get' + | 'errors' + | 'create' + | 'bulkCreate' + | 'delete' + | 'find' + | 'bulkGet' + | 'update' + | 'bulkUpdate' +>; + +export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; +export type ExportedEventNotes = NoteSavedObject[]; + +export interface ExportedNotes { + eventNotes: ExportedEventNotes; + globalNotes: ExportedGlobalNotes; +} + +export type ExportedTimelines = TimelineSavedObject & + ExportedNotes & { + pinnedEventIds: string[]; + }; + +export interface ExportTimelineNotFoundError { + statusCode: number; + message: string; +} + +export interface BulkGetInput { + type: string; + id: string; +} + +export type NotesAndPinnedEventsByTimelineId = Record< + string, + { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } +>; diff --git a/x-pack/plugins/siem/common/types/timeline/note/index.ts b/x-pack/plugins/siem/common/types/timeline/note/index.ts new file mode 100644 index 0000000000000..c8e674997c19c --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/note/index.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedNoteRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.partial({ + eventId: unionWithNullType(runtimeTypes.string), + note: unionWithNullType(runtimeTypes.string), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const NoteSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedNoteRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + noteId: runtimeTypes.string, + timelineVersion: runtimeTypes.union([ + runtimeTypes.string, + runtimeTypes.null, + runtimeTypes.undefined, + ]), + }), +]); + +export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ + SavedNoteRuntimeType, + runtimeTypes.type({ + noteId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface NoteSavedObject + extends runtimeTypes.TypeOf<typeof NoteSavedObjectToReturnRuntimeType> {} diff --git a/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts new file mode 100644 index 0000000000000..89a619598f7c1 --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: runtimeTypes.string, + eventId: runtimeTypes.string, + }), + runtimeTypes.partial({ + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedPinnedEvent extends runtimeTypes.TypeOf<typeof SavedPinnedEventRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedPinnedEventRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + pinnedEventId: unionWithNullType(runtimeTypes.string), + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + pinnedEventId: runtimeTypes.string, + version: runtimeTypes.string, + }), + SavedPinnedEventRuntimeType, + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface PinnedEventSavedObject + extends runtimeTypes.TypeOf<typeof PinnedEventToReturnSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index b46ccdbbe3d05..a12dd926a9181 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 1eb1a7dbde876..e0ff82eb10eb2 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 31c930dce71c0..829332918d3c4 100644 --- a/x-pack/plugins/siem/package.json +++ b/x-pack/plugins/siem/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", @@ -15,6 +15,7 @@ "@types/lodash": "^4.14.110" }, "dependencies": { - "lodash": "^4.17.15" + "lodash": "^4.17.15", + "react-markdown": "^4.0.6" } } diff --git a/x-pack/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 44c1c923cd6ee..6e2a4642f99a4 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 0000000000000..7275a718564ef --- /dev/null +++ b/x-pack/plugins/siem/public/app/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { AppMountParameters } from '../../../../../src/core/public'; +import { StartServices } from '../plugin'; +import { SiemApp } from './app'; + +export const renderApp = (services: StartServices, { element }: AppMountParameters) => { + render(<SiemApp services={services} />, element); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx rename to x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx index dd608babef48f..d545a071c3ea6 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; -import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; import { StatefulEventsViewer } from '../events_viewer'; import * as i18n from './translations'; import { alertsDefaultModel } from './default_headers'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts b/x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx new file mode 100644 index 0000000000000..957feb6244792 --- /dev/null +++ b/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useEffect, useCallback, useMemo } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { AlertsComponentsQueryProps } from './types'; +import { AlertsTable } from './alerts_table'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../lib/kibana'; +import { MatrixHistogramContainer } from '../matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +const ID = 'alertsOverTimeQuery'; + +export const AlertsView = ({ + deleteQuery, + endDate, + filterQuery, + pageFilters, + setQuery, + startDate, + type, +}: AlertsComponentsQueryProps) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const getSubtitle = useCallback( + (totalCount: number) => + `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( + totalCount + )}`, + [] + ); + const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + subtitle: getSubtitle, + }), + [getSubtitle] + ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + return ( + <> + <MatrixHistogramContainer + endDate={endDate} + filterQuery={filterQuery} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={startDate} + type={type} + {...alertsHistogramConfigs} + /> + <AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} /> + </> + ); +}; +AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/plugins/siem/public/components/alerts_viewer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/translations.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/plugins/siem/public/components/alerts_viewer/types.ts new file mode 100644 index 0000000000000..321f7214c8fef --- /dev/null +++ b/x-pack/plugins/siem/public/components/alerts_viewer/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; +import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; +import { MatrixHistogramOption } from '../matrix_histogram/types'; + +type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; +export interface AlertsComponentsQueryProps + extends Pick< + CommonQueryProps, + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' + > { + pageFilters: Filter[]; + stackByOptions?: MatrixHistogramOption[]; + defaultFilters?: Filter[]; + defaultStackByOption?: MatrixHistogramOption; +} diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx rename to x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx diff --git a/x-pack/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx new file mode 100644 index 0000000000000..28355372df146 --- /dev/null +++ b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +const RoundedBadge = (styled(EuiBadge)` + align-items: center; + border-radius: 100%; + display: inline-flex; + font-size: 9px; + height: 34px; + justify-content: center; + margin: 0 5px 0 5px; + padding: 7px 6px 4px 6px; + user-select: none; + width: 34px; + + .euiBadge__content { + position: relative; + top: -1px; + } + + .euiBadge__text { + text-overflow: clip; + } +` as unknown) as typeof EuiBadge; + +RoundedBadge.displayName = 'RoundedBadge'; + +export type AndOr = 'and' | 'or'; + +/** Displays AND / OR in a round badge */ +// Ref: https://github.com/elastic/eui/issues/1655 +export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { + return ( + <RoundedBadge data-test-subj="and-or-badge" color="hollow"> + {type === 'and' ? i18n.AND : i18n.OR} + </RoundedBadge> + ); +}); + +AndOrBadge.displayName = 'AndOrBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/translations.ts b/x-pack/plugins/siem/public/components/and_or_badge/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/and_or_badge/translations.ts rename to x-pack/plugins/siem/public/components/and_or_badge/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/helpers.test.ts b/x-pack/plugins/siem/public/components/arrows/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/helpers.test.ts rename to x-pack/plugins/siem/public/components/arrows/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/helpers.ts b/x-pack/plugins/siem/public/components/arrows/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/helpers.ts rename to x-pack/plugins/siem/public/components/arrows/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/plugins/siem/public/components/arrows/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx rename to x-pack/plugins/siem/public/components/arrows/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/plugins/siem/public/components/arrows/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/index.tsx rename to x-pack/plugins/siem/public/components/arrows/index.tsx diff --git a/x-pack/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 8f261da629f94..dccc156ff6e44 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 0000000000000..72236d799f995 --- /dev/null +++ b/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx @@ -0,0 +1,385 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFieldSearch } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, shallow } from 'enzyme'; +import { noop } from 'lodash/fp'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; + +import { TestProviders } from '../../mock'; + +import { AutocompleteField } from '.'; + +const mockAutoCompleteData: QuerySuggestion[] = [ + { + type: QuerySuggestionTypes.Field, + text: 'agent.ephemeral_id ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.ephemeral_id</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.hostname ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.hostname</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.id ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.id</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.name ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.name</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.type ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.type</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.version ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.version</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test1 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test1</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test2 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test2</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test3 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test3</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test4 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test4</span></p>', + start: 0, + end: 1, + }, +]; + +describe('Autocomplete', () => { + describe('rendering', () => { + test('it renders against snapshot', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = shallow( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder={placeholder} + suggestions={[]} + value={''} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it is rendering with placeholder', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder={placeholder} + suggestions={[]} + value={''} + /> + ); + const input = wrapper.find('input[type="search"]'); + expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); + }); + + test('Rendering suggested items', () => { + const wrapper = mount( + <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + </ThemeProvider> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); + }); + + test('Should Not render suggested items if loading new suggestions', () => { + const wrapper = mount( + <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> + <AutocompleteField + isLoadingSuggestions={true} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + </ThemeProvider> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); + }); + }); + + describe('events', () => { + test('OnChange should have been called', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); + expect(onChange).toHaveBeenCalled(); + }); + }); + + test('OnSubmit should have been called by keying enter on the search input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={true} + loadSuggestions={noop} + onChange={noop} + onSubmit={onSubmit} + placeholder="" + suggestions={mockAutoCompleteData} + value={'filter: query'} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnSubmit should have been called by onSearch event on the input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={true} + loadSuggestions={noop} + onChange={noop} + onSubmit={onSubmit} + placeholder="" + suggestions={mockAutoCompleteData} + value={'filter: query'} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); + // TODO: FixedEuiFieldSearch fails to import + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (wrapperFixedEuiFieldSearch as any).props().onSearch(); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnChange should have been called if keying enter on a suggested item selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when a suggested item is selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { + const onChange = jest.fn((value: string) => value); + const onlyOneSuggestion = [mockAutoCompleteData[0]]; + + const wrapper = mount( + <TestProviders> + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={onlyOneSuggestion} + value={''} + /> + </TestProviders> + ); + + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('Load more suggestions when arrowdown on the search bar', () => { + const loadSuggestions = jest.fn(noop); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={loadSuggestions} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); + expect(loadSuggestions).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx new file mode 100644 index 0000000000000..9821bb6048b51 --- /dev/null +++ b/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx @@ -0,0 +1,333 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFieldSearch, + EuiFieldSearchProps, + EuiOutsideClickDetector, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; + +import euiStyled from '../../../../../legacy/common/eui_styled_components'; + +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + 'data-test-subj'?: string; + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.PureComponent< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + 'data-test-subj': dataTestSubj, + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + return ( + <EuiOutsideClickDetector onOutsideClick={this.handleBlur}> + <AutocompleteContainer> + <FixedEuiFieldSearch + data-test-subj={dataTestSubj} + fullWidth + inputRef={this.handleChangeInputRef} + isLoading={isLoadingSuggestions} + isInvalid={!isValid} + onChange={this.handleChange} + onFocus={this.handleFocus} + onKeyDown={this.handleKeyDown} + onKeyUp={this.handleKeyUp} + onSearch={this.submit} + placeholder={placeholder} + value={value} + /> + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + <SuggestionsPanel> + {suggestions.map((suggestion, suggestionIndex) => ( + <SuggestionItem + key={suggestion.text} + suggestion={suggestion} + isSelected={suggestionIndex === selectedIndex} + onMouseEnter={this.selectSuggestionAt(suggestionIndex)} + onClick={this.applySuggestionAt(suggestionIndex)} + /> + ))} + </SuggestionsPanel> + ) : null} + </AutocompleteContainer> + </EuiOutsideClickDetector> + ); + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => { + const { suggestions } = this.props; + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Tab': + evt.preventDefault(); + if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { + this.applySuggestionAt(0)(); + } else if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } + break; + case 'Escape': + evt.preventDefault(); + evt.stopPropagation(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent<HTMLInputElement>) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); + }; +} + +type StateUpdater<State, Props = {}> = ( + prevState: Readonly<State>, + prevProps: Readonly<Props> +) => State | null; + +function composeStateUpdaters<State, Props>(...updaters: Array<StateUpdater<State, Props>>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, +}); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +export const FixedEuiFieldSearch: React.FC<React.InputHTMLAttributes<HTMLInputElement> & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + }> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +AutocompleteContainer.displayName = 'AutocompleteContainer'; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow: hidden; + z-index: ${props => props.theme.eui.euiZLevel1}; +`; + +SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx rename to x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx index f99a545d558f7..be9a9817265b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx @@ -8,8 +8,8 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import euiStyled from '../../../../../common/eui_styled_components'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; +import euiStyled from '../../../../../legacy/common/eui_styled_components'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/plugins/siem/public/components/bytes/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx rename to x-pack/plugins/siem/public/components/bytes/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx b/x-pack/plugins/siem/public/components/bytes/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/bytes/index.tsx rename to x-pack/plugins/siem/public/components/bytes/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx rename to x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx rename to x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/translations.ts b/x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/translations.ts rename to x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap rename to x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap rename to x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/plugins/siem/public/components/charts/areachart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx rename to x-pack/plugins/siem/public/components/charts/areachart.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/plugins/siem/public/components/charts/areachart.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx rename to x-pack/plugins/siem/public/components/charts/areachart.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/plugins/siem/public/components/charts/barchart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx rename to x-pack/plugins/siem/public/components/charts/barchart.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/plugins/siem/public/components/charts/barchart.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx rename to x-pack/plugins/siem/public/components/charts/barchart.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx b/x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx rename to x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx b/x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx rename to x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx b/x-pack/plugins/siem/public/components/charts/common.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx rename to x-pack/plugins/siem/public/components/charts/common.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/plugins/siem/public/components/charts/common.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/charts/common.tsx rename to x-pack/plugins/siem/public/components/charts/common.tsx index c7b40c50ffde8..74d728e65f018 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/plugins/siem/public/components/charts/common.tsx @@ -19,7 +19,7 @@ import { import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { useUiSetting } from '../../lib/kibana'; export const defaultChartHeight = '100%'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts b/x-pack/plugins/siem/public/components/charts/translation.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/translation.ts rename to x-pack/plugins/siem/public/components/charts/translation.ts diff --git a/x-pack/legacy/plugins/siem/public/components/direction/direction.test.tsx b/x-pack/plugins/siem/public/components/direction/direction.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/direction/direction.test.tsx rename to x-pack/plugins/siem/public/components/direction/direction.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/plugins/siem/public/components/direction/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/direction/index.tsx rename to x-pack/plugins/siem/public/components/direction/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index f8b5eb7209ff4..1d9508fc28f3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -13,7 +13,7 @@ import { wait } from '../../lib/helpers'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline/timeline_context'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts b/x-pack/plugins/siem/public/components/drag_and_drop/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx rename to x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/translations.ts b/x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/field_badge/translations.ts rename to x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/plugins/siem/public/components/draggables/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx rename to x-pack/plugins/siem/public/components/draggables/index.test.tsx diff --git a/x-pack/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 0000000000000..d2dd3e5429341 --- /dev/null +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLink, EuiText } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { createPortalNode, InPortal } from 'react-reverse-portal'; +import styled, { css } from 'styled-components'; + +import { EmbeddablePanel, ErrorEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; +import { useIndexPatterns } from '../../hooks/use_index_patterns'; +import { Loader } from '../loader'; +import { displayErrorToast, useStateToaster } from '../toasters'; +import { Embeddable } from './embeddable'; +import { EmbeddableHeader } from './embeddable_header'; +import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; +import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; +import { MapToolTip } from './map_tool_tip/map_tool_tip'; +import * as i18n from './translations'; +import { SetQuery } from './types'; +import { MapEmbeddable } from '../../../../../legacy/plugins/maps/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { useKibana, useUiSetting$ } from '../../lib/kibana'; +import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public'; + +interface EmbeddableMapProps { + maintainRatio?: boolean; +} + +const EmbeddableMap = styled.div.attrs(() => ({ + className: 'siemEmbeddable__map', +}))<EmbeddableMapProps>` + .embPanel { + border: none; + box-shadow: none; + } + + .mapToolbarOverlay__button { + display: none; + } + + ${({ maintainRatio }) => + maintainRatio && + css` + padding-top: calc(3 / 4 * 100%); /* 4:3 (standard) ratio */ + position: relative; + + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + padding-top: calc(9 / 32 * 100%); /* 32:9 (ultra widescreen) ratio */ + } + + @media only screen and (min-width: 1441px) and (min-height: 901px) { + padding-top: calc(9 / 21 * 100%); /* 21:9 (ultrawide) ratio */ + } + + .embPanel { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + } + `} +`; +EmbeddableMap.displayName = 'EmbeddableMap'; + +export interface EmbeddedMapProps { + query: Query; + filters: Filter[]; + startDate: number; + endDate: number; + setQuery: SetQuery; +} + +export const EmbeddedMapComponent = ({ + endDate, + filters, + query, + setQuery, + startDate, +}: EmbeddedMapProps) => { + const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | undefined | ErrorEmbeddable>( + undefined + ); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [isIndexError, setIsIndexError] = useState(false); + + const [, dispatchToaster] = useStateToaster(); + const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); + const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + + // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our + // own component tree instead of the embeddables (default). This is necessary to have access to + // the Redux store, theme provider, etc, which is required to register and un-register the draggable + // Search InPortal/OutPortal for implementation touch points + const portalNode = React.useMemo(() => createPortalNode(), []); + + const { services } = useKibana(); + + // Initial Load useEffect + useEffect(() => { + let isSubscribed = true; + async function setupEmbeddable() { + // Ensure at least one `siem:defaultIndex` kibana index pattern exists before creating embeddable + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns, + siemDefaultIndices, + }); + + if (matchingIndexPatterns.length === 0 && isSubscribed) { + setIsLoading(false); + setIsIndexError(true); + return; + } + + // Create & set Embeddable + try { + const embeddableObject = await createEmbeddable( + filters, + getIndexPatternTitleIdMapping(matchingIndexPatterns), + query, + startDate, + endDate, + setQuery, + portalNode, + services.embeddable + ); + if (isSubscribed) { + setEmbeddable(embeddableObject); + } + } catch (e) { + if (isSubscribed) { + displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, [e.message], dispatchToaster); + setIsError(true); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + + if (!loadingKibanaIndexPatterns) { + setupEmbeddable(); + } + return () => { + isSubscribed = false; + }; + }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); + + // queryExpression updated useEffect + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ query }); + } + }, [query]); + + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ filters }); + } + }, [filters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && startDate != null && endDate != null) { + const timeRange = { + from: new Date(startDate).toISOString(), + to: new Date(endDate).toISOString(), + }; + embeddable.updateInput({ timeRange }); + } + }, [startDate, endDate]); + + return isError ? null : ( + <Embeddable> + <EmbeddableHeader title={i18n.EMBEDDABLE_HEADER_TITLE}> + <EuiText size="xs"> + <EuiLink + href={`${services.docLinks.ELASTIC_WEBSITE_URL}guide/en/siem/guide/${services.docLinks.DOC_LINK_VERSION}/conf-map-ui.html`} + target="_blank" + > + {i18n.EMBEDDABLE_HEADER_HELP} + </EuiLink> + </EuiText> + </EmbeddableHeader> + + <InPortal node={portalNode}> + <MapToolTip /> + </InPortal> + + <EmbeddableMap maintainRatio={!isIndexError}> + {embeddable != null ? ( + <EmbeddablePanel + data-test-subj="embeddable-panel" + embeddable={embeddable} + getActions={services.uiActions.getTriggerCompatibleActions} + getEmbeddableFactory={services.embeddable.getEmbeddableFactory} + getAllEmbeddableFactories={services.embeddable.getEmbeddableFactories} + notifications={services.notifications} + overlays={services.overlays} + inspector={services.inspector} + application={services.application} + SavedObjectFinder={getSavedObjectFinder(services.savedObjects, services.uiSettings)} + /> + ) : !isLoading && isIndexError ? ( + <IndexPatternsMissingPrompt data-test-subj="missing-prompt" /> + ) : ( + <Loader data-test-subj="loading-panel" overlay size="xl" /> + )} + </EmbeddableMap> + </Embeddable> + ); +}; + +EmbeddedMapComponent.displayName = 'EmbeddedMapComponent'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); + +EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx index f4e6ee5f878a6..aaae43d9684af 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; -import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; import { createPortalNode } from 'react-reverse-portal'; import { mockAPMIndexPattern, @@ -16,10 +16,9 @@ import { mockGlobIndexPattern, } from './__mocks__/mock'; -jest.mock('ui/new_platform'); +const mockEmbeddable = embeddablePluginMock.createStartContract(); -const { npStart } = createUiNewPlatformMock(); -npStart.plugins.embeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ +mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ create: () => ({ reload: jest.fn(), setRenderTooltipContent: jest.fn(), @@ -39,7 +38,7 @@ describe('embedded_map_helpers', () => { 0, setQueryMock, createPortalNode(), - npStart.plugins.embeddable + mockEmbeddable ); expect(setQueryMock).toHaveBeenCalledTimes(1); }); @@ -54,7 +53,7 @@ describe('embedded_map_helpers', () => { 0, setQueryMock, createPortalNode(), - npStart.plugins.embeddable + mockEmbeddable ); expect(setQueryMock.mock.calls[0][0].refetch).not.toBe(embeddable.reload); setQueryMock.mock.results[0].value(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx rename to x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 0c7a1212ba280..dd7e1cd6ea9ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -10,21 +10,21 @@ import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { IndexPatternMapping, SetQuery } from './types'; import { getLayerList } from './map_config'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/public'; import { MapEmbeddable, RenderTooltipContentParams, MapEmbeddableInput, -} from '../../../../maps/public'; +} from '../../../../../legacy/plugins/maps/public'; import * as i18n from './translations'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; import { EmbeddableStart, isErrorEmbeddable, EmbeddableOutput, ViewMode, ErrorEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -109,7 +109,7 @@ export const createEmbeddable = async ( if (!isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - embeddableObject.setLayerList(getLayerList(indexPatterns)); + await embeddableObject.setLayerList(getLayerList(indexPatterns)); } // Wire up to app refresh action diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx b/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx rename to x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts b/x-pack/plugins/siem/public/components/embeddables/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts rename to x-pack/plugins/siem/public/components/embeddables/map_config.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/plugins/siem/public/components/embeddables/map_config.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts rename to x-pack/plugins/siem/public/components/embeddables/map_config.ts index 8c96e0b75a136..0d1cd515820c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/plugins/siem/public/components/embeddables/map_config.ts @@ -13,7 +13,7 @@ import { LayerMappingDetails, } from './types'; import * as i18n from './translations'; -import { SOURCE_TYPES } from '../../../../../../plugins/maps/common/constants'; +import { SOURCE_TYPES } from '../../../../maps/common/constants'; const euiVisColorPalette = euiPaletteColorBlind(); // Update field mappings to modify what fields will be returned to map tooltip diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx similarity index 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 ef2cd85667408..7c2d5e51d813f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -17,10 +17,10 @@ import { import { FeatureProperty } from '../types'; import * as i18n from '../translations'; -const FlowBadge = styled(EuiBadge)` +const FlowBadge = (styled(EuiBadge)` height: 45px; min-width: 85px; -` as 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 0000000000000..d8e20c7f47b4e --- /dev/null +++ b/x-pack/plugins/siem/public/components/embeddables/types.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RenderTooltipContentParams } from '../../../../../legacy/plugins/maps/public'; +import { inputsModel } from '../../store/inputs'; + +export interface IndexPatternMapping { + title: string; + id: string; +} + +export interface LayerMappingDetails { + metricField: string; + geoField: string; + tooltipProperties: string[]; + label: string; +} + +export interface LayerMapping { + source: LayerMappingDetails; + destination: LayerMappingDetails; +} + +export interface LayerMappingCollection { + [indexPatternTitle: string]: LayerMapping; +} + +export type SetQuery = (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; +}) => void; + +export interface MapFeature { + id: number; + layerId: string; +} + +export interface LoadFeatureProps { + layerId: string; + featureId: number; +} + +export interface FeatureProperty { + _propertyKey: string; + _rawValue: string | string[]; +} + +export interface FeatureGeometry { + coordinates: [number]; + type: string; +} + +export type MapToolTipProps = Partial<RenderTooltipContentParams>; diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx b/x-pack/plugins/siem/public/components/empty_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx rename to x-pack/plugins/siem/public/components/empty_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/plugins/siem/public/components/empty_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx rename to x-pack/plugins/siem/public/components/empty_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap rename to x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx rename to x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/index.tsx b/x-pack/plugins/siem/public/components/empty_value/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/index.tsx rename to x-pack/plugins/siem/public/components/empty_value/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/translations.ts b/x-pack/plugins/siem/public/components/empty_value/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/translations.ts rename to x-pack/plugins/siem/public/components/empty_value/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx b/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap rename to x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/plugins/siem/public/components/event_details/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx rename to x-pack/plugins/siem/public/components/event_details/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx rename to x-pack/plugins/siem/public/components/event_details/event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx rename to x-pack/plugins/siem/public/components/event_details/event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx rename to x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx rename to x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_id.ts b/x-pack/plugins/siem/public/components/event_details/event_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_id.ts rename to x-pack/plugins/siem/public/components/event_details/event_id.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx rename to x-pack/plugins/siem/public/components/event_details/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/helpers.tsx rename to x-pack/plugins/siem/public/components/event_details/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx b/x-pack/plugins/siem/public/components/event_details/json_view.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx rename to x-pack/plugins/siem/public/components/event_details/json_view.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/plugins/siem/public/components/event_details/json_view.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx rename to x-pack/plugins/siem/public/components/event_details/json_view.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx rename to x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/translations.ts b/x-pack/plugins/siem/public/components/event_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/translations.ts rename to x-pack/plugins/siem/public/components/event_details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/types.ts b/x-pack/plugins/siem/public/components/event_details/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/types.ts rename to x-pack/plugins/siem/public/components/event_details/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/default_headers.tsx b/x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/default_headers.tsx rename to x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx b/x-pack/plugins/siem/public/components/events_viewer/default_model.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx rename to x-pack/plugins/siem/public/components/events_viewer/default_model.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx rename to x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx rename to x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx rename to x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx index d210c749dae9c..aff66396af39d 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -27,12 +27,7 @@ import { TimelineRefetch } from '../timeline/refetch_timeline'; import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; +import { Filter, esQuery, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx rename to x-pack/plugins/siem/public/components/events_viewer/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.tsx new file mode 100644 index 0000000000000..bc6a1b3b77bfa --- /dev/null +++ b/x-pack/plugins/siem/public/components/events_viewer/index.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { + ColumnHeaderOptions, + SubsetTimelineModel, + TimelineModel, +} from '../../store/timeline/model'; +import { OnChangeItemsPerPage } from '../timeline/events'; +import { Filter } from '../../../../../../src/plugins/data/public'; +import { useUiSetting } from '../../lib/kibana'; +import { EventsViewer } from './events_viewer'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { TimelineTypeContextProps } from '../timeline/timeline_context'; +import { InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; + +export interface OwnProps { + defaultIndices?: string[]; + defaultModel: SubsetTimelineModel; + end: number; + id: string; + start: number; + headerFilterGroup?: React.ReactNode; + pageFilters?: Filter[]; + timelineTypeContext?: TimelineTypeContextProps; + utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; +} + +type Props = OwnProps & PropsFromRedux; + +const defaultTimelineTypeContext = { + loadingText: i18n.LOADING_EVENTS, +}; + +const StatefulEventsViewerComponent: React.FC<Props> = ({ + createTimeline, + columns, + dataProviders, + deletedEventIds, + defaultIndices, + deleteEventQuery, + end, + filters, + headerFilterGroup, + id, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + pageFilters, + query, + removeColumn, + start, + showCheckboxes, + showRowRenderers, + sort, + timelineTypeContext = defaultTimelineTypeContext, + updateItemsPerPage, + upsertColumn, + utilityBar, +}) => { + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + defaultIndices ?? useUiSetting<string[]>(DEFAULT_INDEX_KEY) + ); + + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + } + return () => { + deleteEventQuery({ id, inputId: 'global' }); + }; + }, []); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), + [id, updateItemsPerPage] + ); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id, upsertColumn, removeColumn] + ); + + const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); + + return ( + <InspectButtonContainer> + <EventsViewer + browserFields={browserFields} + columns={columns} + id={id} + dataProviders={dataProviders!} + deletedEventIds={deletedEventIds} + end={end} + filters={globalFilters} + headerFilterGroup={headerFilterGroup} + indexPattern={indexPatterns} + isLive={isLive} + itemsPerPage={itemsPerPage!} + itemsPerPageOptions={itemsPerPageOptions!} + kqlMode={kqlMode} + onChangeItemsPerPage={onChangeItemsPerPage} + query={query} + start={start} + sort={sort!} + timelineTypeContext={timelineTypeContext} + toggleColumn={toggleColumn} + utilityBar={utilityBar} + /> + </InspectButtonContainer> + ); +}; + +const makeMapStateToProps = () => { + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getEvents = timelineSelectors.getEventsByIdSelector(); + const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { + const input: inputsModel.InputsRange = getInputsTimeline(state); + const events: TimelineModel = getEvents(state, id) ?? defaultModel; + const { + columns, + dataProviders, + deletedEventIds, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + sort, + showCheckboxes, + showRowRenderers, + } = events; + + return { + columns, + dataProviders, + deletedEventIds, + filters: getGlobalFiltersQuerySelector(state), + id, + isLive: input.policy.kind === 'interval', + itemsPerPage, + itemsPerPageOptions, + kqlMode, + query: getGlobalQuerySelector(state), + sort, + showCheckboxes, + showRowRenderers, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + createTimeline: timelineActions.createTimeline, + deleteEventQuery: inputsActions.deleteOneQuery, + updateItemsPerPage: timelineActions.updateItemsPerPage, + removeColumn: timelineActions.removeColumn, + upsertColumn: timelineActions.upsertColumn, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulEventsViewer = connector( + React.memo( + StatefulEventsViewerComponent, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.deletedEventIds === nextProps.deletedEventIds && + prevProps.end === nextProps.end && + deepEqual(prevProps.filters, nextProps.filters) && + prevProps.isLive === nextProps.isLive && + prevProps.itemsPerPage === nextProps.itemsPerPage && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + prevProps.kqlMode === nextProps.kqlMode && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.sort, nextProps.sort) && + prevProps.start === nextProps.start && + deepEqual(prevProps.pageFilters, nextProps.pageFilters) && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.start === nextProps.start && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + prevProps.utilityBar === nextProps.utilityBar + ) +); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts b/x-pack/plugins/siem/public/components/events_viewer/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts rename to x-pack/plugins/siem/public/components/events_viewer/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/translations.ts b/x-pack/plugins/siem/public/components/events_viewer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/translations.ts rename to x-pack/plugins/siem/public/components/events_viewer/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx b/x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx rename to x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx b/x-pack/plugins/siem/public/components/external_link_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx rename to x-pack/plugins/siem/public/components/external_link_icon/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap rename to x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx rename to x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx rename to x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx rename to x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/plugins/siem/public/components/fields_browser/category.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_title.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_items.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_name.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx rename to x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/header.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/plugins/siem/public/components/fields_browser/header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx rename to x-pack/plugins/siem/public/components/fields_browser/header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.tsx rename to x-pack/plugins/siem/public/components/fields_browser/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx rename to x-pack/plugins/siem/public/components/fields_browser/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts b/x-pack/plugins/siem/public/components/fields_browser/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts rename to x-pack/plugins/siem/public/components/fields_browser/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts b/x-pack/plugins/siem/public/components/fields_browser/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts rename to x-pack/plugins/siem/public/components/fields_browser/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/plugins/siem/public/components/filter_popover/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx rename to x-pack/plugins/siem/public/components/filter_popover/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap rename to x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx rename to x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/plugins/siem/public/components/filters_global/filters_global.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx rename to x-pack/plugins/siem/public/components/filters_global/filters_global.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/index.tsx b/x-pack/plugins/siem/public/components/filters_global/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/index.tsx rename to x-pack/plugins/siem/public/components/filters_global/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap rename to x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap rename to x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/translations.ts b/x-pack/plugins/siem/public/components/flow_controls/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/translations.ts rename to x-pack/plugins/siem/public/components/flow_controls/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx b/x-pack/plugins/siem/public/components/flyout/button/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx rename to x-pack/plugins/siem/public/components/flyout/button/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/translations.ts b/x-pack/plugins/siem/public/components/flyout/button/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/button/translations.ts rename to x-pack/plugins/siem/public/components/flyout/button/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/components/flyout/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx rename to x-pack/plugins/siem/public/components/flyout/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/flyout/index.tsx b/x-pack/plugins/siem/public/components/flyout/index.tsx new file mode 100644 index 0000000000000..404ca4a16e0f1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/flyout/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import styled from 'styled-components'; + +import { State, timelineSelectors } from '../../store'; +import { DataProvider } from '../timeline/data_providers/data_provider'; +import { FlyoutButton } from './button'; +import { Pane } from './pane'; +import { timelineActions } from '../../store/actions'; +import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; + +export const Badge = (styled(EuiBadge)` + position: absolute; + padding-left: 4px; + padding-right: 4px; + right: 0%; + top: 0%; + border-bottom-left-radius: 5px; +` as unknown) as typeof EuiBadge; + +Badge.displayName = 'Badge'; + +const Visible = styled.div<{ show?: boolean }>` + visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; +`; + +Visible.displayName = 'Visible'; + +interface OwnProps { + flyoutHeight: number; + timelineId: string; + usersViewing: string[]; +} + +type Props = OwnProps & ProsFromRedux; + +export const FlyoutComponent = React.memo<Props>( + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { + const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ + showTimeline, + timelineId, + ]); + const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ + showTimeline, + timelineId, + ]); + + return ( + <> + <Visible show={show}> + <Pane + flyoutHeight={flyoutHeight} + onClose={handleClose} + timelineId={timelineId} + width={width} + > + <StatefulTimeline onClose={handleClose} usersViewing={usersViewing} id={timelineId} /> + </Pane> + </Visible> + <FlyoutButton + dataProviders={dataProviders} + show={!show} + timelineId={timelineId} + onOpen={handleOpen} + /> + </> + ); + } +); + +FlyoutComponent.displayName = 'FlyoutComponent'; + +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + +const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; + + return { dataProviders, show, width }; +}; + +const mapDispatchToProps = { + showTimeline: timelineActions.showTimeline, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type ProsFromRedux = ConnectedProps<typeof connector>; + +export const Flyout = connector(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx b/x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/plugins/siem/public/components/flyout/pane/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts rename to x-pack/plugins/siem/public/components/flyout/pane/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx rename to x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx new file mode 100644 index 0000000000000..98a1acf471629 --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +type Bytes = string | number; + +export const formatBytes = (value: Bytes, format: string) => { + return numeral(value).format(format); +}; + +export const useFormatBytes = () => { + const [bytesFormat] = useUiSetting$<string>(DEFAULT_BYTES_FORMAT); + + return (value: Bytes) => formatBytes(value, bytesFormat); +}; + +export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( + <>{useFormatBytes()(value)}</> +); + +PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; + +export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); + +PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx rename to x-pack/plugins/siem/public/components/formatted_date/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx rename to x-pack/plugins/siem/public/components/formatted_date/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.test.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.test.ts rename to x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.ts rename to x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.test.ts b/x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.test.ts rename to x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.tsx b/x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/plugins/siem/public/components/formatted_duration/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/translations.ts b/x-pack/plugins/siem/public/components/formatted_duration/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/translations.ts rename to x-pack/plugins/siem/public/components/formatted_duration/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/plugins/siem/public/components/formatted_ip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx rename to x-pack/plugins/siem/public/components/formatted_ip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/index.test.tsx b/x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/index.test.tsx rename to x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/index.tsx b/x-pack/plugins/siem/public/components/generic_downloader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/index.tsx rename to x-pack/plugins/siem/public/components/generic_downloader/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/translations.ts b/x-pack/plugins/siem/public/components/generic_downloader/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/translations.ts rename to x-pack/plugins/siem/public/components/generic_downloader/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/plugins/siem/public/components/header_global/index.test.tsx new file mode 100644 index 0000000000000..0f6c5c2e139a7 --- /dev/null +++ b/x-pack/plugins/siem/public/components/header_global/index.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import '../../mock/match_media'; +import { HeaderGlobal } from './index'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/app/siem#/hosts/allHosts', + hash: '', + search: '', + state: '', + }), + withRouter: () => jest.fn(), +})); + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar +jest.mock('../search_bar', () => ({ + SiemSearchBar: () => null, +})); + +describe('HeaderGlobal', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it renders', () => { + const wrapper = shallow(<HeaderGlobal />); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/plugins/siem/public/components/header_global/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/index.tsx rename to x-pack/plugins/siem/public/components/header_global/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/translations.ts b/x-pack/plugins/siem/public/components/header_global/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/translations.ts rename to x-pack/plugins/siem/public/components/header_global/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/editable_title.test.tsx rename to x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/plugins/siem/public/components/header_page/editable_title.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx rename to x-pack/plugins/siem/public/components/header_page/editable_title.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/plugins/siem/public/components/header_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx rename to x-pack/plugins/siem/public/components/header_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/index.tsx b/x-pack/plugins/siem/public/components/header_page/index.tsx new file mode 100644 index 0000000000000..e1559cf9e0c48 --- /dev/null +++ b/x-pack/plugins/siem/public/components/header_page/index.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; +import { Title } from './title'; +import { DraggableArguments, BadgeOptions, TitleProp } from './types'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})<HeaderProps>` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = (styled(EuiBadge)` + letter-spacing: 0; +` as unknown) as typeof EuiBadge; +Badge.displayName = 'Badge'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; + dataTestSubj?: string; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + subtitle?: SubtitleProps['items']; + subtitle2?: SubtitleProps['items']; + title: TitleProp; + titleNode?: React.ReactElement; +} + +const HeaderPageComponent: React.FC<HeaderPageProps> = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isLoading, + subtitle, + subtitle2, + title, + titleNode, + ...rest +}) => ( + <Header border={border} {...rest}> + <EuiFlexGroup alignItems="center"> + <FlexItem> + {backOptions && ( + <LinkBack> + <LinkIcon + dataTestSubj={backOptions.dataTestSubj} + href={backOptions.href} + iconType="arrowLeft" + > + {backOptions.text} + </LinkIcon> + </LinkBack> + )} + + {titleNode || ( + <Title + draggableArguments={draggableArguments} + title={title} + badgeOptions={badgeOptions} + /> + )} + + {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} + {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} + {border && isLoading && <EuiProgress size="xs" color="accent" />} + </FlexItem> + + {children && ( + <FlexItem data-test-subj="header-page-supplements" grow={false}> + {children} + </FlexItem> + )} + </EuiFlexGroup> + </Header> +); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.test.tsx b/x-pack/plugins/siem/public/components/header_page/title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/title.test.tsx rename to x-pack/plugins/siem/public/components/header_page/title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx b/x-pack/plugins/siem/public/components/header_page/title.tsx similarity index 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 47dd0dc55d703..43b50c24f6b5b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/plugins/siem/public/components/header_page/title.tsx @@ -17,9 +17,9 @@ const StyledEuiBetaBadge = styled(EuiBetaBadge)` StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` letter-spacing: 0; -` as 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 0000000000000..6d473f4721710 --- /dev/null +++ b/x-pack/plugins/siem/public/components/links/index.tsx @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { isNil } from 'lodash/fp'; +import styled from 'styled-components'; + +import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; +import { + DefaultFieldRendererOverflow, + DEFAULT_MORE_MAX_HEIGHT, +} from '../field_renderers/field_renderers'; +import { encodeIpv6 } from '../../lib/helpers'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; +import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { useUiSetting$ } from '../../lib/kibana'; +import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; +import { ExternalLinkIcon } from '../external_link_icon'; +import { navTabs } from '../../pages/home/home_navigations'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; + +import * as i18n from './translations'; + +export const DEFAULT_NUMBER_OF_LINK = 5; + +// Internal Links +const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ + children, + hostName, +}) => ( + <EuiLink href={getHostDetailsUrl(encodeURIComponent(hostName))}> + {children ? children : hostName} + </EuiLink> +); + +const whitelistUrlSchemes = ['http://', 'https://']; +export const ExternalLink = React.memo<{ + url: string; + children?: React.ReactNode; + idx?: number; + overflowIndexStart?: number; + allItemsLimit?: number; +}>( + ({ + url, + children, + idx, + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + }) => { + const lastVisibleItemIndex = overflowIndexStart - 1; + const lastItemIndex = allItemsLimit - 1; + const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); + const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); + return url && inWhitelist && !isUrlInvalid(url) && children ? ( + <EuiToolTip content={url} position="top" data-test-subj="externalLinkTooltip"> + <EuiLink href={url} target="_blank" rel="noopener" data-test-subj="externalLink"> + {children} + <ExternalLinkIcon data-test-subj="externalLinkIcon" /> + {!isNil(idx) && idx < lastIndexToShow && <Comma data-test-subj="externalLinkComma" />} + </EuiLink> + </EuiToolTip> + ) : null; + } +); + +ExternalLink.displayName = 'ExternalLink'; + +export const HostDetailsLink = React.memo(HostDetailsLinkComponent); + +const IPDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + ip: string; + flowTarget?: FlowTarget | FlowTargetSourceDest; +}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( + <EuiLink href={`${getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget)}`}> + {children ? children : ip} + </EuiLink> +); + +export const IPDetailsLink = React.memo(IPDetailsLinkComponent); + +const CaseDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + detailName: string; + title?: string; +}> = ({ children, detailName, title }) => { + const search = useGetUrlSearch(navTabs.case); + + return ( + <EuiLink + href={getCaseDetailsUrl({ id: detailName, search })} + data-test-subj="case-details-link" + aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)} + > + {children ? children : detailName} + </EuiLink> + ); +}; +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { + const search = useGetUrlSearch(navTabs.case); + return <EuiLink href={getCreateCaseUrl(search)}>{children}</EuiLink>; +}); + +CreateCaseLink.displayName = 'CreateCaseLink'; + +// External Links +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( + ({ children, link }) => ( + <ExternalLink url={`https://www.google.com/search?q=${encodeURIComponent(link)}`}> + {children ? children : link} + </ExternalLink> + ) +); + +GoogleLink.displayName = 'GoogleLink'; + +export const PortOrServiceNameLink = React.memo<{ + children?: React.ReactNode; + portOrServiceName: number | string; +}>(({ children, portOrServiceName }) => ( + <EuiLink + data-test-subj="port-or-service-name-link" + href={`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent( + String(portOrServiceName) + )}`} + target="_blank" + > + {children ? children : portOrServiceName} + </EuiLink> +)); + +PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; + +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + <EuiLink + data-test-subj="ja3-fingerprint-link" + href={`https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`} + target="_blank" + > + {children ? children : ja3Fingerprint} + </EuiLink> +)); + +Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; + +export const CertificateFingerprintLink = React.memo<{ + children?: React.ReactNode; + certificateFingerprint: string; +}>(({ children, certificateFingerprint }) => ( + <EuiLink + data-test-subj="certificate-fingerprint-link" + href={`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent( + certificateFingerprint + )}`} + target="_blank" + > + {children ? children : certificateFingerprint} + </EuiLink> +)); + +CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; + +enum DefaultReputationLink { + 'virustotal.com' = 'virustotal.com', + 'talosIntelligence.com' = 'talosIntelligence.com', +} + +export interface ReputationLinkSetting { + name: string; + url_template: string; +} + +function isDefaultReputationLink(name: string): name is DefaultReputationLink { + return ( + name === DefaultReputationLink['virustotal.com'] || + name === DefaultReputationLink['talosIntelligence.com'] + ); +} +const isReputationLink = ( + rowItem: string | ReputationLinkSetting +): rowItem is ReputationLinkSetting => + (rowItem as ReputationLinkSetting).url_template !== undefined && + (rowItem as ReputationLinkSetting).name !== undefined; + +export const Comma = styled('span')` + margin-right: 5px; + margin-left: 5px; + &::after { + content: ' ,'; + } +`; + +Comma.displayName = 'Comma'; + +const defaultNameMapping: Record<DefaultReputationLink, string> = { + [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, + [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, +}; + +const ReputationLinkComponent: React.FC<{ + overflowIndexStart?: number; + allItemsLimit?: number; + showDomain?: boolean; + domain: string; + direction?: 'row' | 'column'; +}> = ({ + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + showDomain = false, + domain, + direction = 'row', +}) => { + const [ipReputationLinksSetting] = useUiSetting$<ReputationLinkSetting[]>( + IP_REPUTATION_LINKS_SETTING + ); + + const ipReputationLinks: ReputationLinkSetting[] = useMemo( + () => + ipReputationLinksSetting + ?.slice(0, allItemsLimit) + .filter( + ({ url_template, name }) => + !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) + ) + .map(({ name, url_template }: { name: string; url_template: string }) => ({ + name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, + url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), + })), + [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] + ); + + return ipReputationLinks?.length > 0 ? ( + <section> + <EuiFlexGroup + gutterSize="none" + justifyContent="center" + direction={direction} + alignItems="center" + data-test-subj="reputationLinkGroup" + > + <EuiFlexItem grow={true}> + {ipReputationLinks + ?.slice(0, overflowIndexStart) + .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( + <ExternalLink + allItemsLimit={ipReputationLinks.length} + idx={id} + overflowIndexStart={overflowIndexStart} + url={urlTemplate} + data-test-subj="externalLinkComponent" + key={`reputationLink-${id}`} + > + <>{showDomain ? domain : name ?? domain}</> + </ExternalLink> + ))} + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <DefaultFieldRendererOverflow + rowItems={ipReputationLinks} + idPrefix="moreReputationLink" + render={rowItem => { + return ( + isReputationLink(rowItem) && ( + <ExternalLink + url={rowItem.url_template} + overflowIndexStart={overflowIndexStart} + allItemsLimit={allItemsLimit} + > + <>{rowItem.name ?? domain}</> + </ExternalLink> + ) + ); + }} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={overflowIndexStart} + /> + </EuiFlexItem> + </EuiFlexGroup> + </section> + ) : null; +}; + +ReputationLinkComponent.displayName = 'ReputationLinkComponent'; + +export const ReputationLink = React.memo(ReputationLinkComponent); + +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( + ({ children, domain }) => ( + <ExternalLink url={`https://www.iana.org/whois?q=${encodeURIComponent(domain)}`}> + {children ? children : domain} + </ExternalLink> + ) +); + +WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/legacy/plugins/siem/public/components/links/translations.ts b/x-pack/plugins/siem/public/components/links/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/links/translations.ts rename to x-pack/plugins/siem/public/components/links/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx b/x-pack/plugins/siem/public/components/loader/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx rename to x-pack/plugins/siem/public/components/loader/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/plugins/siem/public/components/loader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/index.tsx rename to x-pack/plugins/siem/public/components/loader/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/plugins/siem/public/components/loading/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loading/index.tsx rename to x-pack/plugins/siem/public/components/loading/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx b/x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx rename to x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx b/x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx rename to x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap rename to x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx b/x-pack/plugins/siem/public/components/markdown/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx rename to x-pack/plugins/siem/public/components/markdown/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx b/x-pack/plugins/siem/public/components/markdown/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/index.tsx rename to x-pack/plugins/siem/public/components/markdown/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx rename to x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx rename to x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/translations.ts b/x-pack/plugins/siem/public/components/markdown/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/translations.ts rename to x-pack/plugins/siem/public/components/markdown/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts b/x-pack/plugins/siem/public/components/markdown_editor/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts rename to x-pack/plugins/siem/public/components/markdown_editor/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx b/x-pack/plugins/siem/public/components/markdown_editor/form.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx rename to x-pack/plugins/siem/public/components/markdown_editor/form.tsx diff --git a/x-pack/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 0000000000000..c4bdff7ea649a --- /dev/null +++ b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKI gltyKM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`; + +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKI RNnzH\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/translations.ts b/x-pack/plugins/siem/public/components/matrix_histogram/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/translations.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/translations.ts diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/plugins/siem/public/components/matrix_histogram/types.ts new file mode 100644 index 0000000000000..c59775ad325d0 --- /dev/null +++ b/x-pack/plugins/siem/public/components/matrix_histogram/types.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTitleSize } from '@elastic/eui'; +import { ScaleType, Position, TickFormatter } from '@elastic/charts'; +import { ActionCreator } from 'redux'; +import { ESQuery } from '../../../common/typed_json'; +import { SetQuery } from '../../pages/hosts/navigation/types'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../graphql/types'; +import { UpdateDateRange } from '../charts/common'; + +export type MatrixHistogramMappingTypes = Record< + string, + { key: string; value: null; color?: string | undefined } +>; +export interface MatrixHistogramOption { + text: string; + value: string; +} + +export type GetSubTitle = (count: number) => string; +export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; + +export interface MatrixHisrogramConfigs { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + titleSize?: EuiTitleSize; +} + +interface MatrixHistogramBasicProps { + chartHeight?: number; + defaultIndex: string[]; + defaultStackByOption: MatrixHistogramOption; + dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + endDate: number; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + id: string; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + panelHeight?: number; + setQuery: SetQuery; + startDate: number; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title?: string | GetTitle; + titleSize?: EuiTitleSize; +} + +export interface MatrixHistogramQueryProps { + endDate: number; + errorMessage: string; + filterQuery?: ESQuery | string | undefined; + setAbsoluteRangeDatePicker?: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + stackByField: string; + startDate: number; + indexToAdd?: string[] | null; + isInspected: boolean; + histogramType: HistogramType; +} + +export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + scaleType?: ScaleType; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; + showSpacer?: boolean; + legendPosition?: Position; +} + +export interface HistogramBucket { + key_as_string: string; + key: number; + doc_count: number; +} +export interface GroupBucket { + key: string; + signals: { + buckets: HistogramBucket[]; + }; +} + +export interface HistogramAggregation { + histogramAgg: { + buckets: GroupBucket[]; + }; +} + +export interface BarchartConfigs { + series: { + xScaleType: ScaleType; + yScaleType: ScaleType; + stackAccessors: string[]; + }; + axis: { + xTickFormatter: TickFormatter; + yTickFormatter: TickFormatter; + tickSize: number; + }; + settings: { + legendPosition: Position; + onBrushEnd: UpdateDateRange; + showLegend: boolean; + showLegendExtra: boolean; + theme: { + scales: { + barsPadding: number; + }; + chartMargins: { + left: number; + right: number; + top: number; + bottom: number; + }; + chartPaddings: { + left: number; + right: number; + top: number; + bottom: number; + }; + }; + }; + customHeight: number; +} diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts b/x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/plugins/siem/public/components/matrix_histogram/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx rename to x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts b/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts b/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/translations.ts b/x-pack/plugins/siem/public/components/ml/anomaly/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/translations.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts index cebfc172ee6ff..d64bd3a64e941 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts @@ -6,7 +6,7 @@ import { useState, useEffect } from 'react'; -import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts rename to x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts b/x-pack/plugins/siem/public/components/ml/api/errors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts rename to x-pack/plugins/siem/public/components/ml/api/errors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts rename to x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts b/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts rename to x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts b/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts rename to x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts b/x-pack/plugins/siem/public/components/ml/api/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts rename to x-pack/plugins/siem/public/components/ml/api/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx rename to x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx index b5aacdf664c67..b7c544273ae92 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -14,7 +14,7 @@ import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_hel import { SiemPageName } from '../../../pages/home/types'; import { HostsTableType } from '../../../store/hosts/model'; -import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx rename to x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx index e27e9dc084c57..54773e3ab6dda 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -13,7 +13,7 @@ import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers'; import { SiemPageName } from '../../../pages/home/types'; -import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.ts rename to x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.ts b/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.ts rename to x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/empty_ml_capabilities.ts b/x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/empty_ml_capabilities.ts rename to x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx rename to x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx b/x-pack/plugins/siem/public/components/ml/entity_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx rename to x-pack/plugins/siem/public/components/ml/entity_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/get_entries.test.ts b/x-pack/plugins/siem/public/components/ml/get_entries.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/get_entries.test.ts rename to x-pack/plugins/siem/public/components/ml/get_entries.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/get_entries.ts b/x-pack/plugins/siem/public/components/ml/get_entries.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/get_entries.ts rename to x-pack/plugins/siem/public/components/ml/get_entries.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx b/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx rename to x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx b/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx rename to x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.test.ts rename to x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.ts b/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.ts rename to x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.test.ts b/x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.test.ts rename to x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.ts b/x-pack/plugins/siem/public/components/ml/links/create_series_link.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.ts rename to x-pack/plugins/siem/public/components/ml/links/create_series_link.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/mock.ts b/x-pack/plugins/siem/public/components/ml/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/mock.ts rename to x-pack/plugins/siem/public/components/ml/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx rename to x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/translations.ts b/x-pack/plugins/siem/public/components/ml/permissions/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/translations.ts rename to x-pack/plugins/siem/public/components/ml/permissions/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx b/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx rename to x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx index 24f203a3682d5..e7615bf3b89ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx +++ b/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx @@ -8,7 +8,7 @@ import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic import React from 'react'; import styled from 'styled-components'; -import { DescriptionList } from '../../../../../../../plugins/siem/common/utility_types'; +import { DescriptionList } from '../../../../common/utility_types'; import { Anomaly, NarrowDateRange } from '../types'; import { getScoreString } from './score_health'; import { PreferenceFormattedDate } from '../../formatted_date'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts b/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts rename to x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts b/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts rename to x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx b/x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx b/x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx rename to x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_score_string.test.ts b/x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_score_string.test.ts rename to x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.test.ts b/x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.test.ts rename to x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.ts b/x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.ts rename to x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_health.tsx b/x-pack/plugins/siem/public/components/ml/score/score_health.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_health.tsx rename to x-pack/plugins/siem/public/components/ml/score/score_health.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts b/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts rename to x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts b/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts rename to x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/translations.ts b/x-pack/plugins/siem/public/components/ml/score/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/translations.ts rename to x-pack/plugins/siem/public/components/ml/score/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/basic_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/basic_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.test.ts b/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.ts b/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.ts rename to x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.test.ts b/x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.ts b/x-pack/plugins/siem/public/components/ml/tables/host_equality.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.ts rename to x-pack/plugins/siem/public/components/ml/tables/host_equality.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.test.ts b/x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.ts b/x-pack/plugins/siem/public/components/ml/tables/network_equality.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.ts rename to x-pack/plugins/siem/public/components/ml/tables/network_equality.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/translations.ts b/x-pack/plugins/siem/public/components/ml/tables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/translations.ts rename to x-pack/plugins/siem/public/components/ml/tables/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/types.test.ts b/x-pack/plugins/siem/public/components/ml/types.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/types.test.ts rename to x-pack/plugins/siem/public/components/ml/types.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/types.ts b/x-pack/plugins/siem/public/components/ml/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/types.ts rename to x-pack/plugins/siem/public/components/ml/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/plugins/siem/public/components/ml_popover/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx rename to x-pack/plugins/siem/public/components/ml_popover/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx b/x-pack/plugins/siem/public/components/ml_popover/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx rename to x-pack/plugins/siem/public/components/ml_popover/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx index bc488ee00988b..7bcbf4afa10cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { checkRecognizer, getJobsSummary, getModules } from '../api'; import { SiemJob } from '../types'; import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx index a0343608dc67a..e7b14f2e80bf2 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx @@ -11,7 +11,7 @@ import { isJobLoading, isJobFailed, isJobStarted, -} from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +} from '../../../../common/detection_engine/ml_helpers'; import { SiemJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index bd7d696757ca6..3c93e1c195cd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -9,7 +9,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { MlPopover } from './ml_popover'; -jest.mock('ui/new_platform'); jest.mock('../../lib/kibana'); jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx b/x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx rename to x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml_popover/types.ts b/x-pack/plugins/siem/public/components/ml_popover/types.ts new file mode 100644 index 0000000000000..58d40c298b329 --- /dev/null +++ b/x-pack/plugins/siem/public/components/ml_popover/types.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AuditMessageBase } from '../../../../ml/common/types/audit_message'; +import { MlError } from '../ml/types'; + +export interface Group { + id: string; + jobIds: string[]; + calendarIds: string[]; +} + +export interface CheckRecognizerProps { + indexPatternName: string[]; + signal: AbortSignal; +} + +export interface RecognizerModule { + id: string; + title: string; + query: Record<string, object>; + description: string; + logo: { + icon: string; + }; +} + +export interface GetModulesProps { + moduleId?: string; + signal: AbortSignal; +} + +export interface Module { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: Record<string, object>; + jobs: ModuleJob[]; + datafeeds: ModuleDatafeed[]; + kibana: object; +} + +/** + * Representation of an ML Job as returned from `the ml/modules/get_module` API + */ +export interface ModuleJob { + id: string; + config: { + groups: string[]; + description: string; + analysis_config: { + bucket_span: string; + summary_count_field_name?: string; + detectors: Detector[]; + influencers: string[]; + }; + analysis_limits: { + model_memory_limit: string; + }; + data_description: { + time_field: string; + time_format?: string; + }; + model_plot_config?: { + enabled: boolean; + }; + custom_settings: { + created_by: string; + custom_urls: CustomURL[]; + }; + job_type: string; + }; +} + +// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices +// See mockGetModuleResponse for examples +export interface ModuleDatafeed { + id: string; + config: { + job_id: string; + indexes?: string[]; + indices?: string[]; + query: Record<string, object>; + }; +} + +export interface MlSetupArgs { + configTemplate: string; + indexPatternName: string; + jobIdErrorFilter: string[]; + groups: string[]; + prefix?: string; +} + +/** + * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API + */ +export interface JobSummary { + auditMessage?: AuditMessageBase; + datafeedId: string; + datafeedIndices: string[]; + datafeedState: string; + description: string; + earliestTimestampMs?: number; + latestResultsTimestampMs?: number; + groups: string[]; + hasDatafeed: boolean; + id: string; + isSingleMetricViewerJob: boolean; + jobState: string; + latestTimestampMs?: number; + memory_status: string; + nodeName?: string; + processed_record_count: number; +} + +export interface Detector { + detector_description: string; + function: string; + by_field_name: string; + partition_field_name?: string; +} + +export interface CustomURL { + url_name: string; + url_value: string; +} + +/** + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * that includes necessary metadata like moduleName, defaultIndexPattern, etc. + */ +export interface SiemJob extends JobSummary { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isInstalled: boolean; + isElasticJob: boolean; +} + +export interface AugmentedSiemJobFields { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isElasticJob: boolean; +} + +export interface SetupMlResponseJob { + id: string; + success: boolean; + error?: MlError; +} + +export interface SetupMlResponseDatafeed { + id: string; + success: boolean; + started: boolean; + error?: MlError; +} + +export interface SetupMlResponse { + jobs: SetupMlResponseJob[]; + datafeeds: SetupMlResponseDatafeed[]; + kibana: {}; +} + +export interface StartDatafeedResponse { + [key: string]: { + started: boolean; + error?: string; + }; +} + +export interface ErrorResponse { + statusCode?: number; + error?: string; + message?: string; +} + +export interface StopDatafeedResponse { + [key: string]: { + stopped: boolean; + }; +} + +export interface CloseJobsResponse { + [key: string]: { + closed: boolean; + }; +} + +export interface JobsFilters { + filterQuery: string; + showCustomJobs: boolean; + showElasticJobs: boolean; + selectedGroups: string[]; +} diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx b/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx rename to x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts rename to x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts new file mode 100644 index 0000000000000..85d77485830a5 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, omit } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { APP_NAME } from '../../../../common/constants'; +import { StartServices } from '../../../plugin'; +import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; +import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; +import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; +import { SiemPageName } from '../../../pages/home/types'; +import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; +import { getOverviewUrl } from '../../link_to'; + +import { TabNavigationProps } from '../tab_navigation/types'; +import { getSearch } from '../helpers'; +import { SearchNavTab } from '../types'; + +export const setBreadcrumbs = ( + spyState: RouteSpyState & TabNavigationProps, + chrome: StartServices['chrome'] +) => { + const breadcrumbs = getBreadcrumbsForRoute(spyState); + if (breadcrumbs) { + chrome.setBreadcrumbs(breadcrumbs); + } +}; + +export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ + { + text: APP_NAME, + href: getOverviewUrl(), + }, +]; + +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.network; + +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.hosts; + +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + +const isDetectionsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SiemPageName.detections; + +export const getBreadcrumbsForRoute = ( + object: RouteSpyState & TabNavigationProps +): ChromeBreadcrumb[] | null => { + const spyState: RouteSpyState = omit('navTabs', object); + if (isHostsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getHostDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isNetworkRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getIPDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isDetectionsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getDetectionRulesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isCaseRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getCaseDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if ( + spyState != null && + object.navTabs && + spyState.pageName && + object.navTabs[spyState.pageName] + ) { + return [ + ...siemRootBreadcrumb, + { + text: object.navTabs[spyState.pageName].name, + href: '', + }, + ]; + } + + return null; +}; diff --git a/x-pack/plugins/siem/public/components/navigation/helpers.ts b/x-pack/plugins/siem/public/components/navigation/helpers.ts new file mode 100644 index 0000000000000..291cb90098f78 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/helpers.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { Location } from 'history'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { CONSTANTS } from '../url_state/constants'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; +import { + replaceQueryStringInLocation, + replaceStateKeyInQueryString, + getQueryStringFromLocation, +} from '../url_state/helpers'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; + +import { SearchNavTab } from './types'; + +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { + if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { + return URL_STATE_KEYS[tab.urlKey].reduce<Location>( + (myLocation: Location, urlKey: KeyUrlState) => { + let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; + + if (urlKey === CONSTANTS.appQuery && urlState.query != null) { + if (urlState.query.query === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.query; + } + } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { + if (isEmpty(urlState.filters)) { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.filters; + } + } else if (urlKey === CONSTANTS.timerange) { + urlStateToReplace = urlState[CONSTANTS.timerange]; + } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { + const timeline = urlState[CONSTANTS.timeline]; + if (timeline.id === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = timeline; + } + } + return replaceQueryStringInLocation( + myLocation, + replaceStateKeyInQueryString( + urlKey, + urlStateToReplace + )(getQueryStringFromLocation(myLocation.search)) + ); + }, + { + pathname: '', + hash: '', + search: '', + state: '', + } + ).search; + } + return ''; +}; diff --git a/x-pack/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/index.test.tsx new file mode 100644 index 0000000000000..d8b62029138c8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/index.test.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { CONSTANTS } from '../url_state/constants'; +import { SiemNavigationComponent } from './'; +import { setBreadcrumbs } from './breadcrumbs'; +import { navTabs } from '../../pages/home/home_navigations'; +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from '../../utils/route/types'; +import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; + +jest.mock('./breadcrumbs', () => ({ + setBreadcrumbs: jest.fn(), +})); + +describe('SIEM Navigation', () => { + const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs, + urlState: { + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }, + }; + const wrapper = mount(<SiemNavigationComponent {...mockProps} />); + test('it calls setBreadcrumbs with correct path on mount', () => { + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + search: '', + tabName: 'authentications', + query: { query: '', language: 'kuery' }, + filters: [], + savedQuery: undefined, + timeline: { + id: '', + isOpen: false, + }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); + test('it calls setBreadcrumbs with correct path on update', () => { + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + filters: [], + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + query: { language: 'kuery', query: '' }, + savedQuery: undefined, + search: '', + state: undefined, + tabName: 'authentications', + timeline: { id: '', isOpen: false }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/plugins/siem/public/components/navigation/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/index.tsx rename to x-pack/plugins/siem/public/components/navigation/index.tsx diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx new file mode 100644 index 0000000000000..99ded06cfdcc8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { navTabs } from '../../../pages/home/home_navigations'; +import { SiemPageName } from '../../../pages/home/types'; +import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; +import { HostsTableType } from '../../../store/hosts/model'; +import { RouteSpyState } from '../../../utils/route/types'; +import { CONSTANTS } from '../../url_state/constants'; +import { TabNavigationComponent } from './'; +import { TabNavigationProps } from './types'; + +describe('Tab Navigation', () => { + const pageName = SiemPageName.hosts; + const hostName = 'siem-window'; + const tabName = HostsTableType.authentications; + const pathName = `/${pageName}/${hostName}/${tabName}`; + + describe('Page Navigation', () => { + const mockProps: TabNavigationProps & RouteSpyState = { + pageName, + pathName, + detailName: undefined, + search: '', + tabName, + navTabs, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); + expect(hostsTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); + expect(networkTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(networkTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); + expect(firstTab.props().href).toBe( + "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" + ); + }); + }); + + describe('Table Navigation', () => { + const mockHasMlUserPermissions = true; + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const tableNavigationTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + + expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const tableNavigationTab = () => + wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); + expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: SiemPageName.hosts, + pathName: `/${SiemPageName.hosts}`, + tabName: HostsTableType.events, + }); + wrapper.update(); + expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const firstTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + expect(firstTab.props().href).toBe( + `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx rename to x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts new file mode 100644 index 0000000000000..2e2dea09f8c38 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UrlInputsModel } from '../../../store/inputs/model'; +import { CONSTANTS } from '../../url_state/constants'; +import { HostsTableType } from '../../../store/hosts/model'; +import { TimelineUrl } from '../../../store/timeline/model'; +import { Filter, Query } from '../../../../../../../src/plugins/data/public'; + +import { SiemNavigationProps } from '../types'; + +export interface TabNavigationProps extends SiemNavigationProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; +} + +export interface TabNavigationItemProps { + href: string; + hrefWithSearch: string; + id: string; + disabled: boolean; + name: string; + isSelected: boolean; +} diff --git a/x-pack/plugins/siem/public/components/navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/types.ts new file mode 100644 index 0000000000000..e8a2865938062 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter, Query } from '../../../../../../src/plugins/data/public'; +import { HostsTableType } from '../../store/hosts/model'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { CONSTANTS, UrlStateType } from '../url_state/constants'; + +export interface SiemNavigationProps { + display?: 'default' | 'condensed'; + navTabs: Record<string, NavTab>; +} + +export interface SiemNavigationComponentProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + urlState: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; + }; +} + +export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; + +export interface NavTab { + id: string; + name: string; + href: string; + disabled: boolean; + urlKey: UrlStateType; + isDetailPage?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/use_get_url_search.tsx b/x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/use_get_url_search.tsx rename to x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx b/x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx rename to x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/plugins/siem/public/components/netflow/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx rename to x-pack/plugins/siem/public/components/netflow/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx b/x-pack/plugins/siem/public/components/netflow/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/index.tsx rename to x-pack/plugins/siem/public/components/netflow/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/types.ts b/x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/types.ts rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/types.ts b/x-pack/plugins/siem/public/components/netflow/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/types.ts rename to x-pack/plugins/siem/public/components/netflow/types.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts new file mode 100644 index 0000000000000..96bd9b08bf8bf --- /dev/null +++ b/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts @@ -0,0 +1,492 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../common/constants'; +import { KibanaServices } from '../../lib/kibana'; +import { rawNewsApiResponse } from '../../mock/news'; +import { rawNewsJSON } from '../../mock/raw_news'; + +import { + fetchNews, + getLocale, + getNewsFeedUrl, + getNewsItemsFromApiResponse, + removeSnapshotFromVersion, + showNewsItem, +} from './helpers'; +import { NewsItem, RawNewsApiResponse } from './types'; + +jest.mock('../../lib/kibana'); + +describe('helpers', () => { + describe('removeSnapshotFromVersion', () => { + test('it should remove an all-caps `-SNAPSHOT`', () => { + const version = '8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove a mixed-case `-SnApShoT`', () => { + const version = '8.0.0-SnApShoT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { + const version = '-SNAPSHOT8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { + const version = '8.0.0'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { + const version = '8.0.0SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); + }); + + test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { + const version = '8.0.0-SNAP'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); + }); + + test('it should NOT transform an undefined version', () => { + const version = undefined; + + expect(removeSnapshotFromVersion(version)).toBeUndefined(); + }); + + test('it should NOT transform an empty version', () => { + const version = ''; + + expect(removeSnapshotFromVersion(version)).toEqual(''); + }); + }); + + describe('getNewsFeedUrl', () => { + const getKibanaVersion = () => '8.0.0'; + + test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { + expect( + getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { + const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { + const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { + const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; + + expect( + getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { + const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; + + expect( + getNewsFeedUrl({ + newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, + getKibanaVersion: getKibanaVersionWithSnapshot, + }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + }); + + describe('getLocale', () => { + const fallback = 'wowzers'; + + test('it returns language specified in the document', () => { + const lang = 'ja'; + + document.documentElement.lang = lang; + + expect(getLocale(fallback)).toEqual(lang); + }); + + test('it returns the fallback when the language in the document is an empty string', () => { + document.documentElement.lang = ''; + + expect(getLocale(fallback)).toEqual(fallback); + }); + }); + + describe('getNewsItemsFromApiResponse', () => { + const expectedNewsItems: NewsItem[] = [ + { + description: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + expireOn: expect.any(Date), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Got SIEM Questions?', + }, + { + description: + 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', + expireOn: expect.any(Date), + hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Security 7.5.0 released', + }, + { + description: + 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', + expireOn: expect.any(Date), + hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Endpoint Security Overview Webinar', + }, + { + description: + 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', + expireOn: expect.any(Date), + hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Trying Elastic SIEM at Home?', + }, + { + description: + 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', + expireOn: expect.any(Date), + hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Introducing Elastic Endpoint Security', + }, + { + description: + 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', + expireOn: expect.any(Date), + hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'What is Elastic Common Schema (ECS)?', + }, + ]; + + test('it returns an empty collection of news items when the response is undefined', () => { + expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response is null', () => { + expect(getNewsItemsFromApiResponse(null)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are undefined', () => { + expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are null', () => { + expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); + }); + + test('it returns the expected news items when the browser language matches the i18n values in the response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { + const allCapsLang = 'EN'; + + document.documentElement.lang = allCapsLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { + const nonMatchingLang = 'ja'; + + document.documentElement.lang = nonMatchingLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language is an empty string', () => { + const emptyLang = ''; + + document.documentElement.lang = emptyLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news item when parsing a raw JSON response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); + }); + + describe('translated items', () => { + const translatedDescription = + 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; + const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; + const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; + const translatedTitle = 'SIEMに関する質問はありますか?'; + + const withNonDefaultTranslations: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?', ja: translatedTitle }, + description: { + en: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + ja: translatedDescription, + }, + link_text: null, + link_url: { + en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + ja: translatedLinkUrl, + }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + ja: translatedImageUrl, + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + test('it returns a translated description when the browser language matches additional translated content', () => { + const lang = 'ja'; // an additional translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( + translatedDescription + ); + }); + + test('it returns a translated imageUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( + translatedImageUrl + ); + }); + + test('it returns a translated linkUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( + translatedLinkUrl + ); + }); + + test('it returns a translated title when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + translatedTitle + ); + }); + + test('it returns the default translated title when the browser language matches additional translated content', () => { + const lang = 'fr'; // no translation for this language + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + + test('it returns the default translated title when the browser language is an empty string', () => { + const lang = ''; // just an empty string + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + }); + + test('it generates a news item hash when an item does NOT include it', () => { + const lang = 'en'; + + const itemHasNoHash: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?' }, + description: { + en: 'some description', + }, + link_text: null, + link_url: { en: 'https://example.com/link-url' }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: 'https://example.com/image-url', + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); + }); + }); + + describe('fetchNews', () => { + const mockKibanaServices = KibanaServices.get as jest.Mock; + const fetchMock = jest.fn(); + mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rawNewsApiResponse); + }); + + test('it returns the raw API response from the news feed', async () => { + const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; + expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); + }); + }); + + describe('showNewsItem', () => { + const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z + + let dateNowSpy: { mockRestore: () => void }; + + beforeAll(() => { + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); + }); + + afterAll(() => { + dateNowSpy.mockRestore(); + }); + + test('it should return true when the article has already been published, and will expire in the future', () => { + const alreadyPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Show this post', + }; + + expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); + }); + + test('it should return false when the article was published exactly "now", and will expire in the future', () => { + const publishedJustNowAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article has not been published yet, and has not expired yet', () => { + const notPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 5000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW + 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and will expire exactly now', () => { + const alreadyPublishedAndExpiredNow: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and it already expired', () => { + const articleJustExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW - 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 5000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(articleJustExpired)).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts b/x-pack/plugins/siem/public/components/news_feed/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts rename to x-pack/plugins/siem/public/components/news_feed/helpers.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/index.tsx b/x-pack/plugins/siem/public/components/news_feed/index.tsx new file mode 100644 index 0000000000000..ae41737c5bcad --- /dev/null +++ b/x-pack/plugins/siem/public/components/news_feed/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers'; +import { useKibana, useUiSetting$, KibanaServices } from '../../lib/kibana'; +import { NewsFeed } from './news_feed'; +import { NewsItem } from './types'; + +export const StatefulNewsFeed = React.memo<{ + enableNewsFeedSetting: string; + newsFeedSetting: string; +}>(({ enableNewsFeedSetting, newsFeedSetting }) => { + const kibanaNewsfeedEnabled = useKibana().services.newsfeed; + const [enableNewsFeed] = useUiSetting$<boolean>(enableNewsFeedSetting); + const [newsFeedUrlSetting] = useUiSetting$<string>(newsFeedSetting); + const [news, setNews] = useState<NewsItem[] | null>(null); + + // respect kibana's global newsfeed.enabled setting + const newsfeedEnabled = kibanaNewsfeedEnabled && enableNewsFeed; + + const newsFeedUrl = getNewsFeedUrl({ + newsFeedUrlSetting, + getKibanaVersion: () => KibanaServices.getKibanaVersion(), + }); + + useEffect(() => { + let canceled = false; + + const fetchData = async () => { + try { + const apiResponse = await fetchNews({ newsFeedUrl }); + + if (!canceled) { + setNews(getNewsItemsFromApiResponse(apiResponse)); + } + } catch { + if (!canceled) { + setNews([]); + } + } + }; + + if (newsfeedEnabled) { + fetchData(); + } + + return () => { + canceled = true; + }; + }, [newsfeedEnabled, newsFeedUrl]); + + return <>{newsfeedEnabled ? <NewsFeed news={news} /> : null}</>; +}); + +StatefulNewsFeed.displayName = 'StatefulNewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx b/x-pack/plugins/siem/public/components/news_feed/news_feed.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx rename to x-pack/plugins/siem/public/components/news_feed/news_feed.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx b/x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx b/x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx b/x-pack/plugins/siem/public/components/news_feed/post/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/post/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts b/x-pack/plugins/siem/public/components/news_feed/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts rename to x-pack/plugins/siem/public/components/news_feed/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/types.ts b/x-pack/plugins/siem/public/components/news_feed/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/types.ts rename to x-pack/plugins/siem/public/components/news_feed/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap rename to x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx b/x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.tsx b/x-pack/plugins/siem/public/components/notes/add_note/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/index.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx b/x-pack/plugins/siem/public/components/notes/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/columns.tsx rename to x-pack/plugins/siem/public/components/notes/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx b/x-pack/plugins/siem/public/components/notes/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx rename to x-pack/plugins/siem/public/components/notes/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/index.tsx b/x-pack/plugins/siem/public/components/notes/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/index.tsx rename to x-pack/plugins/siem/public/components/notes/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap similarity index 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 0000000000000..12cf952bb1ff8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback } from 'react'; +import { DeleteTimelines } from '../types'; + +import { TimelineDownloader } from './export_timeline'; +import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; +import { exportSelectedTimeline } from '../../../containers/timeline/api'; + +export interface ExportTimeline { + disableExportTimelineDownloader: () => void; + enableExportTimelineDownloader: () => void; + isEnableDownloader: boolean; +} + +export const useExportTimeline = (): ExportTimeline => { + const [isEnableDownloader, setIsEnableDownloader] = useState(false); + + const enableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(true); + }, []); + + const disableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(false); + }, []); + + return { + disableExportTimelineDownloader, + enableExportTimelineDownloader, + isEnableDownloader, + }; +}; + +const EditTimelineActionsComponent: React.FC<{ + deleteTimelines: DeleteTimelines | undefined; + ids: string[]; + isEnableDownloader: boolean; + isDeleteTimelineModalOpen: boolean; + onComplete: () => void; + title: string; +}> = ({ + deleteTimelines, + ids, + isEnableDownloader, + isDeleteTimelineModalOpen, + onComplete, + title, +}) => ( + <> + <TimelineDownloader + exportedIds={ids} + getExportedData={exportSelectedTimeline} + isEnableDownloader={isEnableDownloader} + onComplete={onComplete} + /> + {deleteTimelines != null && ( + <DeleteTimelineModalOverlay + deleteTimelines={deleteTimelines} + isModalOpen={isDeleteTimelineModalOpen} + onComplete={onComplete} + savedObjectIds={ids} + title={title} + /> + )} + </> +); + +export const EditTimelineActions = React.memo(EditTimelineActionsComponent); +export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts rename to x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts rename to x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts rename to x-pack/plugins/siem/public/components/open_timeline/helpers.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx new file mode 100644 index 0000000000000..ea28bc06ef915 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -0,0 +1,649 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import { MockedProvider } from 'react-apollo/test-utils'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../lib/helpers'; +import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; +import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; + +import { NotePreviews } from './note_previews'; +import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; +import { StatefulOpenTimeline } from '.'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all'; +jest.mock('../../lib/kibana'); +jest.mock('../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); + +describe('StatefulOpenTimeline', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const title = 'All Timelines / Open Timelines'; + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it has the expected initial state', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + const componentProps = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .props(); + + expect(componentProps).toEqual({ + ...componentProps, + itemIdToExpandedNotesRowMap: {}, + onlyFavorites: false, + pageIndex: 0, + pageSize: 10, + query: '', + selectedItems: [], + sortDirection: 'desc', + sortField: 'updated', + }); + }); + + describe('#onQueryChange', () => { + test('it updates the query state with the expected trimmed value when the user enters a query', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + expect( + wrapper + .find('[data-test-subj="search-row"]') + .first() + .prop('query') + ).toEqual('abcd'); + }); + + test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines with'); + }); + + test('echos (renders) the query when the user enters a query', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="selectable-query-text"]') + .first() + .text() + ).toEqual('with "abcd"'); + }); + }); + + describe('#focusInput', () => { + test('focuses the input when the component mounts', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + expect( + wrapper + .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) + .first() + .getDOMNode().id === document.activeElement!.id + ).toBe(true); + }); + }); + + describe('#onAddTimelinesToFavorites', () => { + // This functionality is hiding for now and waiting to see the light in the near future + test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { + const addTimelinesToFavorites = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="favorite-selected"]') + .first() + .simulate('click'); + + expect(addTimelinesToFavorites).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onDeleteSelected', () => { + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { + const deleteTimelines = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="delete-selected"]') + .first() + .simulate('click'); + + expect(deleteTimelines).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onSelectionChange', () => { + test('it updates the selection state when timelines are selected', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + const selectedItems: [] = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + + expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query + }); + }); + + describe('#onTableChange', () => { + test('it updates the sort state when the user clicks on a column to sort it', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('desc'); + + wrapper + .find('thead tr th button') + .at(0) + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('asc'); + }); + }); + + describe('#onToggleOnlyFavorites', () => { + test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(false); + + wrapper + .find('[data-test-subj="only-favorites-toggle"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(true); + }); + }); + + describe('#onToggleShowNotes', () => { + test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({ + '10849df0-7b44-11e9-a608-ab3d811609': ( + <NotePreviews + notes={ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes != null + ? mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes.map( + note => ({ ...note, savedObjectId: note.noteId }) + ) + : [] + } + /> + ), + }); + }); + + test('it renders the expanded notes when the expand button is clicked', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); + + expect( + wrapper + .find('[data-test-subj="note-previews-container"]') + .find('[data-test-subj="updated-by"]') + .first() + .text() + ).toEqual('elastic'); + }); + }); + + test('it renders the title', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + expect( + wrapper + .find('[data-test-subj="header-section-title"]') + .first() + .text() + ).toEqual(title); + }); + + describe('#resetSelectionState', () => { + test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + const getSelectedItem = (): [] => + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + await wait(); + expect(getSelectedItem().length).toEqual(0); + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + expect(getSelectedItem().length).toEqual(13); + }); + }); + + test('it renders the expected count of matching timelines when no query has been entered', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <MockedProvider addTypename={false}> + <TestProviderWithoutDragAndDrop> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </TestProviderWithoutDragAndDrop> + </MockedProvider> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines '); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find( + `[data-test-subj="title-${ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId + }"]` + ) + .first() + .simulate('click'); + + expect(onOpenTimeline).toHaveBeenCalledWith({ + duplicate: false, + timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] + .savedObjectId, + }); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="open-duplicate"]') + .first() + .simulate('click'); + + expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx new file mode 100644 index 0000000000000..d26d02780ffba --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -0,0 +1,343 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ApolloClient from 'apollo-client'; +import React, { useEffect, useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { Dispatch } from 'redux'; +import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; +import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; +import { useGetAllTimeline } from '../../containers/timeline/all'; +import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; +import { State, timelineSelectors } from '../../store'; +import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { + createTimeline as dispatchCreateNewTimeline, + updateIsLoading as dispatchUpdateIsLoading, +} from '../../store/timeline/actions'; +import { OpenTimeline } from './open_timeline'; +import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; +import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; +import { + ActionTimelineToShow, + DeleteTimelines, + EuiSearchBarQuery, + OnDeleteSelected, + OnOpenTimeline, + OnQueryChange, + OnSelectionChange, + OnTableChange, + OnTableChangeParams, + OpenTimelineProps, + OnToggleOnlyFavorites, + OpenTimelineResult, + OnToggleShowNotes, + OnDeleteOneTimeline, +} from './types'; +import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; + +interface OwnProps<TCache = object> { + apolloClient: ApolloClient<TCache>; + /** Displays open timeline in modal */ + isModal: boolean; + closeModalTimeline?: () => void; + hideActions?: ActionTimelineToShow[]; + onOpenTimeline?: (timeline: TimelineModel) => void; +} + +export type OpenTimelineOwnProps = OwnProps & + Pick< + OpenTimelineProps, + 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' + > & + PropsFromRedux; + +/** Returns a collection of selected timeline ids */ +export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => + selectedItems.reduce<string[]>( + (validSelections, timelineResult) => + timelineResult.savedObjectId != null + ? [...validSelections, timelineResult.savedObjectId] + : validSelections, + [] + ); + +/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ +export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( + ({ + apolloClient, + closeModalTimeline, + createNewTimeline, + defaultPageSize, + hideActions = [], + isModal = false, + importDataModalToggle, + onOpenTimeline, + setImportDataModalToggle, + timeline, + title, + updateTimeline, + updateIsLoading, + }) => { + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< + Record<string, JSX.Element> + >({}); + /** Only query for favorite timelines when true */ + const [onlyFavorites, setOnlyFavorites] = useState(false); + /** The requested page of results */ + const [pageIndex, setPageIndex] = useState(0); + /** The requested size of each page of search results */ + const [pageSize, setPageSize] = useState(defaultPageSize); + /** The current search criteria */ + const [search, setSearch] = useState(''); + /** The currently-selected timelines in the table */ + const [selectedItems, setSelectedItems] = useState<OpenTimelineResult[]>([]); + /** The requested sort direction of the query results */ + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); + /** The requested field to sort on */ + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + + const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline(); + + /** Invoked when the user presses enters to submit the text in the search input */ + const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { + setSearch(query.queryText.trim()); + }, []); + + /** Focuses the input that filters the field browser */ + const focusInput = () => { + const elements = document.querySelector<HTMLElement>(`.${OPEN_TIMELINE_CLASS_NAME} input`); + + if (elements != null) { + elements.focus(); + } + }; + + /* This feature will be implemented in the near future, so we are keeping it to know what to do */ + + /** Invoked when the user clicks the action to add the selected timelines to favorites */ + // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { + // const { addTimelinesToFavorites } = this.props; + // const { selectedItems } = this.state; + // if (addTimelinesToFavorites != null) { + // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); + // TODO: it's not possible to clear the selection state of the newly-favorited + // items, because we can't pass the selection state as props to the table. + // See: https://github.com/elastic/eui/issues/1077 + // TODO: the query must re-execute to show the results of the mutation + // } + // }; + + const deleteTimelines: DeleteTimelines = useCallback( + async (timelineIds: string[]) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + await apolloClient.mutate< + DeleteTimelineMutation.Mutation, + DeleteTimelineMutation.Variables + >({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, + }); + refetch(); + }, + [apolloClient, createNewTimeline, refetch, timeline] + ); + + const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( + async (timelineIds: string[]) => { + await deleteTimelines(timelineIds); + }, + [deleteTimelines] + ); + + /** Invoked when the user clicks the action to delete the selected timelines */ + const onDeleteSelected: OnDeleteSelected = useCallback(async () => { + await deleteTimelines(getSelectedTimelineIds(selectedItems)); + + // NOTE: we clear the selection state below, but if the server fails to + // delete a timeline, it will remain selected in the table: + resetSelectionState(); + + // TODO: the query must re-execute to show the results of the deletion + }, [selectedItems, deleteTimelines]); + + /** Invoked when the user selects (or de-selects) timelines */ + const onSelectionChange: OnSelectionChange = useCallback( + (newSelectedItems: OpenTimelineResult[]) => { + setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 + }, + [] + ); + + /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ + const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { + const { index, size } = page; + const { field, direction } = sort; + setPageIndex(index); + setPageSize(size); + setSortDirection(direction); + setSortField(field); + }, []); + + /** Invoked when the user toggles the option to only view favorite timelines */ + const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + const onToggleShowNotes: OnToggleShowNotes = useCallback( + (newItemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => { + setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); + }, + [] + ); + + /** Resets the selection state such that all timelines are unselected */ + const resetSelectionState = useCallback(() => { + setSelectedItems([]); + }, []); + + const openTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + if (isModal && closeModalTimeline != null) { + closeModalTimeline(); + } + + queryTimelineById({ + apolloClient, + duplicate, + onOpenTimeline, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + useEffect(() => { + focusInput(); + }, []); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + search, + sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount, + }); + }, [ + pageIndex, + pageSize, + search, + sortField, + sortDirection, + timelines, + totalCount, + onlyFavorites, + ]); + + return !isModal ? ( + <OpenTimeline + data-test-subj={'open-timeline'} + deleteTimelines={onDeleteOneTimeline} + defaultPageSize={defaultPageSize} + isLoading={loading} + itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} + importDataModalToggle={importDataModalToggle} + onAddTimelinesToFavorites={undefined} + onDeleteSelected={onDeleteSelected} + onlyFavorites={onlyFavorites} + onOpenTimeline={openTimeline} + onQueryChange={onQueryChange} + onSelectionChange={onSelectionChange} + onTableChange={onTableChange} + onToggleOnlyFavorites={onToggleOnlyFavorites} + onToggleShowNotes={onToggleShowNotes} + pageIndex={pageIndex} + pageSize={pageSize} + query={search} + refetch={refetch} + searchResults={timelines} + setImportDataModalToggle={setImportDataModalToggle} + selectedItems={selectedItems} + sortDirection={sortDirection} + sortField={sortField} + title={title} + totalSearchResultsCount={totalCount} + /> + ) : ( + <OpenTimelineModalBody + data-test-subj={'open-timeline-modal'} + deleteTimelines={onDeleteOneTimeline} + defaultPageSize={defaultPageSize} + hideActions={hideActions} + isLoading={loading} + itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} + onAddTimelinesToFavorites={undefined} + onlyFavorites={onlyFavorites} + onOpenTimeline={openTimeline} + onQueryChange={onQueryChange} + onSelectionChange={onSelectionChange} + onTableChange={onTableChange} + onToggleOnlyFavorites={onToggleOnlyFavorites} + onToggleShowNotes={onToggleShowNotes} + pageIndex={pageIndex} + pageSize={pageSize} + query={search} + searchResults={timelines} + selectedItems={selectedItems} + sortDirection={sortDirection} + sortField={sortField} + title={title} + totalSearchResultsCount={totalCount} + /> + ); + } +); + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State) => { + const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + return { + timeline, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + createNewTimeline: ({ + id, + columns, + show, + }: { + id: string; + columns: ColumnHeaderOptions[]; + show?: boolean; + }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx index 6b2f953b82de4..26aeab87e3510 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -14,7 +14,7 @@ import { TimelinesTable } from './timelines_table'; import { TitleRow } from './title_row'; import { ImportDataModal } from '../import_data_modal'; import * as i18n from './translations'; -import { importTimelines } from '../../containers/timeline/all/api'; +import { importTimelines } from '../../containers/timeline/api'; import { UtilityBarGroup, diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx new file mode 100644 index 0000000000000..46a0d46c1e0d1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../../lib/helpers'; +import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; +import { useGetAllTimeline, getAllTimeline } from '../../../containers/timeline/all'; + +import { OpenTimelineModal } from '.'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../utils/apollo_context', () => ({ + useApolloClient: () => ({}), +})); +jest.mock('../../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); + +describe('OpenTimelineModal', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it renders the expected modal', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <OpenTimelineModal onClose={jest.fn()} /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts b/x-pack/plugins/siem/public/components/open_timeline/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts rename to x-pack/plugins/siem/public/components/open_timeline/translations.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts new file mode 100644 index 0000000000000..41999c6249277 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SetStateAction, Dispatch } from 'react'; +import { AllTimelinesVariables } from '../../containers/timeline/all'; +import { TimelineModel } from '../../store/timeline/model'; +import { NoteResult } from '../../graphql/types'; +import { Refetch } from '../../store/inputs/model'; +import { TimelineType } from '../../../common/types/timeline'; + +/** The users who added a timeline to favorites */ +export interface FavoriteTimelineResult { + userId?: number | null; + userName?: string | null; + favoriteDate?: number | null; +} + +export interface TimelineResultNote { + savedObjectId?: string | null; + note?: string | null; + noteId?: string | null; + updated?: number | null; + updatedBy?: string | null; +} + +export interface TimelineActionsOverflowColumns { + width: string; + actions: Array<{ + name: string; + icon?: string; + onClick?: (timeline: OpenTimelineResult) => void; + description: string; + render?: (timeline: OpenTimelineResult) => JSX.Element; + } | null>; +} + +/** The results of the query run by the OpenTimeline component */ +export interface OpenTimelineResult { + created?: number | null; + description?: string | null; + eventIdToNoteIds?: Readonly<Record<string, string[]>> | null; + favorite?: FavoriteTimelineResult[] | null; + noteIds?: string[] | null; + notes?: TimelineResultNote[] | null; + pinnedEventIds?: Readonly<Record<string, boolean>> | null; + savedObjectId?: string | null; + title?: string | null; + templateTimelineId?: string | null; + type?: TimelineType.template | TimelineType.default; + updated?: number | null; + updatedBy?: string | null; +} + +/** + * EuiSearchBar returns this object when the user changes the query. At the + * time of this writing, there is no typescript definition for this type, so + * only the properties used by the Open Timeline component are exposed. + */ +export interface EuiSearchBarQuery { + queryText: string; +} + +/** Performs IO to delete the specified timelines */ +export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; + +/** Invoked when the user clicks the action make the selected timelines favorites */ +export type OnAddTimelinesToFavorites = () => void; + +/** Invoked when the user clicks the action to delete the selected timelines */ +export type OnDeleteSelected = () => void; +export type OnDeleteOneTimeline = (timelineIds: string[]) => void; + +/** Invoked when the user clicks on the name of a timeline to open it */ +export type OnOpenTimeline = ({ + duplicate, + timelineId, +}: { + duplicate: boolean; + timelineId: string; +}) => void; + +export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; +export type SetActionTimeline = Dispatch<SetStateAction<OpenTimelineResult | undefined>>; +export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; +/** Invoked when the user presses enters to submit the text in the search input */ +export type OnQueryChange = (query: EuiSearchBarQuery) => void; + +/** Invoked when the user selects (or de-selects) timelines in the table */ +export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; + +/** Invoked when the user toggles the option to only view favorite timelines */ +export type OnToggleOnlyFavorites = () => void; + +/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ +export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => void; + +/** Parameters to the OnTableChange callback */ +export interface OnTableChangeParams { + page: { + index: number; + size: number; + }; + sort: { + field: string; + direction: 'asc' | 'desc'; + }; +} + +/** Invoked by the EUI table implementation when the user interacts with the table */ +export type OnTableChange = (tableChange: OnTableChangeParams) => void; + +export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; + +export interface OpenTimelineProps { + /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ + deleteTimelines?: DeleteTimelines; + /** The default requested size of each page of search results */ + defaultPageSize: number; + /** Displays an indicator that data is loading when true */ + isLoading: boolean; + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + itemIdToExpandedNotesRowMap: Record<string, JSX.Element>; + /** Display import timelines modal*/ + importDataModalToggle?: boolean; + /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; + /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onDeleteSelected?: OnDeleteSelected; + /** Only show favorite timelines when true */ + onlyFavorites: boolean; + /** Invoked when the user presses enter after typing in the search bar */ + onQueryChange: OnQueryChange; + /** Invoked when the user selects (or de-selects) timelines in the table */ + onSelectionChange: OnSelectionChange; + /** Invoked when the user clicks on the name of a timeline to open it */ + onOpenTimeline: OnOpenTimeline; + /** Invoked by the EUI table implementation when the user interacts with the table */ + onTableChange: OnTableChange; + /** Invoked when the user toggles the option to only show favorite timelines */ + onToggleOnlyFavorites: OnToggleOnlyFavorites; + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + onToggleShowNotes: OnToggleShowNotes; + /** the requested page of results */ + pageIndex: number; + /** the requested size of each page of search results */ + pageSize: number; + /** The currently applied search criteria */ + query: string; + /** Refetch table */ + refetch?: Refetch; + /** The results of executing a search */ + searchResults: OpenTimelineResult[]; + /** the currently-selected timelines in the table */ + selectedItems: OpenTimelineResult[]; + /** Toggle export timelines modal*/ + setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>; + /** the requested sort direction of the query results */ + sortDirection: 'asc' | 'desc'; + /** the requested field to sort on */ + sortField: string; + /** The title of the Open Timeline component */ + title: string; + /** The total (server-side) count of the search results */ + totalSearchResultsCount: number; + /** Hide action on timeline if needed it */ + hideActions?: ActionTimelineToShow[]; +} + +export interface UpdateTimeline { + duplicate: boolean; + id: string; + from: number; + notes: NoteResult[] | null | undefined; + timeline: TimelineModel; + to: number; + ruleNote?: string; +} + +export type DispatchUpdateTimeline = ({ + duplicate, + id, + from, + notes, + timeline, + to, + ruleNote, +}: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts new file mode 100644 index 0000000000000..6573b6371e9a4 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from '../../../../../../../src/plugins/data/public'; + +export const createFilter = ( + key: string, + value: string[] | string | null | undefined, + negate: boolean = false +): Filter => { + const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; + return queryValue != null + ? { + meta: { + alias: null, + negate, + disabled: false, + type: 'phrase', + key, + value: queryValue, + params: { + query: queryValue, + }, + }, + query: { + match: { + [key]: { + query: queryValue, + type: 'phrase', + }, + }, + }, + } + : ({ + exists: { + field: key, + }, + meta: { + alias: null, + disabled: false, + key, + negate: value === undefined, + type: 'exists', + value: 'exists', + }, + } as Filter); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx new file mode 100644 index 0000000000000..7aed36422bd2f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { WithHoverActions } from '../../with_hover_actions'; +import { useKibana } from '../../../lib/kibana'; + +import * as i18n from './translations'; + +export * from './helpers'; + +interface OwnProps { + children: JSX.Element; + filter: Filter; + onFilterAdded?: () => void; +} + +export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( + ({ children, filter, onFilterAdded }) => { + const { filterManager } = useKibana().services.data.query; + + const filterForValue = useCallback(() => { + filterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + const filterOutValue = useCallback(() => { + filterManager.addFilters({ + ...filter, + meta: { + ...filter.meta, + negate: true, + }, + }); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + return ( + <WithHoverActions + hoverContent={ + <div data-test-subj="hover-actions-container"> + <EuiToolTip content={i18n.FILTER_FOR_VALUE}> + <EuiButtonIcon + aria-label={i18n.FILTER_FOR_VALUE} + color="text" + data-test-subj="add-to-filter" + iconType="magnifyWithPlus" + onClick={filterForValue} + /> + </EuiToolTip> + + <EuiToolTip content={i18n.FILTER_OUT_VALUE}> + <EuiButtonIcon + aria-label={i18n.FILTER_OUT_VALUE} + color="text" + data-test-subj="filter-out-value" + iconType="magnifyWithMinus" + onClick={filterOutValue} + /> + </EuiToolTip> + </div> + } + render={() => children} + /> + ); + } +); + +AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx new file mode 100644 index 0000000000000..4d0e6a737d303 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { getOr } from 'lodash/fp'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; +import { DescriptionList } from '../../../../../common/utility_types'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { getEmptyTagValue } from '../../../empty_value'; +import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; +import { InspectButton, InspectButtonContainer } from '../../../inspect'; +import { HostItem } from '../../../../graphql/types'; +import { Loader } from '../../../loader'; +import { IPDetailsLink } from '../../../links'; +import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; +import { AnomalyScores } from '../../../ml/score/anomaly_scores'; +import { Anomalies, NarrowDateRange } from '../../../ml/types'; +import { DescriptionListStyled, OverviewWrapper } from '../../index'; +import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host'; + +import * as i18n from './translations'; + +interface HostSummaryProps { + data: HostItem; + id: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + narrowDateRange: NarrowDateRange; +} + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + <EuiFlexItem key={key}> + <DescriptionListStyled listItems={descriptionList} /> + </EuiFlexItem> +); + +export const HostOverview = React.memo<HostSummaryProps>( + ({ + data, + loading, + id, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); + + const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( + <DefaultFieldRenderer + rowItems={getOr([], fieldName, fieldData)} + attrName={fieldName} + idPrefix="host-overview" + /> + ); + + const column: DescriptionList[] = [ + { + title: i18n.HOST_ID, + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), + }, + { + title: i18n.FIRST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + <FirstLastSeenHost + hostname={data.host.name[0]} + type={FirstLastSeenHostType.FIRST_SEEN} + /> + ) : ( + getEmptyTagValue() + ), + }, + { + title: i18n.LAST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + <FirstLastSeenHost + hostname={data.host.name[0]} + type={FirstLastSeenHostType.LAST_SEEN} + /> + ) : ( + getEmptyTagValue() + ), + }, + ]; + const firstColumn = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + <AnomalyScores + anomalies={anomaliesData} + startDate={startDate} + endDate={endDate} + isLoading={isLoadingAnomaliesData} + narrowDateRange={narrowDateRange} + /> + ), + }, + ] + : column; + + const descriptionLists: Readonly<DescriptionList[][]> = [ + firstColumn, + [ + { + title: i18n.IP_ADDRESSES, + description: ( + <DefaultFieldRenderer + rowItems={getOr([], 'host.ip', data)} + attrName={'host.ip'} + idPrefix="host-overview" + render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())} + /> + ), + }, + { + title: i18n.MAC_ADDRESSES, + description: getDefaultRenderer('host.mac', data), + }, + { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, + ], + [ + { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, + { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, + { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, + { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, + ], + [ + { + title: i18n.CLOUD_PROVIDER, + description: getDefaultRenderer('cloud.provider', data), + }, + { + title: i18n.REGION, + description: getDefaultRenderer('cloud.region', data), + }, + { + title: i18n.INSTANCE_ID, + description: getDefaultRenderer('cloud.instance.id', data), + }, + { + title: i18n.MACHINE_TYPE, + description: getDefaultRenderer('cloud.machine.type', data), + }, + ], + ]; + + return ( + <InspectButtonContainer> + <OverviewWrapper> + <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + <Loader + overlay + overlayBackground={ + darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor + } + size="xl" + /> + )} + </OverviewWrapper> + </InspectButtonContainer> + ); + } +); + +HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/index.tsx b/x-pack/plugins/siem/public/components/page/index.tsx new file mode 100644 index 0000000000000..5feb2ef73c57f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/index.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; +import styled, { createGlobalStyle } from 'styled-components'; + +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ +export const AppGlobalStyle = createGlobalStyle` + /* dirty hack to fix draggables with tooltip on FF */ + body#siem-app { + position: static; + } + /* end of dirty hack to fix draggables with tooltip on FF */ + + div.app-wrapper { + background-color: rgba(0,0,0,0); + } + + div.application { + background-color: rgba(0,0,0,0); + } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + min-width: 24px; + } + .euiToolTip { + z-index: 9950 !important; + } + + /* + overrides the default styling of euiComboBoxOptionsList because it's implemented + as a popover, so it's not selectable as a child of the styled component + */ + .euiComboBoxOptionsList { + z-index: 9999; + } + + /* overrides default styling in angular code that was not theme-friendly */ + .euiPanel-loading-hide-border { + border: none; + } + + /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ + body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { + visibility: hidden !important; + } + + /* ensure elastic charts tooltips appear above open euiPopovers */ + .echTooltip { + z-index: 9950; + } + +`; + +export const DescriptionListStyled = styled(EuiDescriptionList)` + ${({ theme }) => ` + dt { + font-size: ${theme.eui.euiFontSizeXS} !important; + } + dd { + width: fit-content; + } + dd > div { + width: fit-content; + } + `} +`; + +DescriptionListStyled.displayName = 'DescriptionListStyled'; + +export const PageContainer = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + height: 100%; + padding: 1rem; + overflow: hidden; + margin: 0px; +`; + +PageContainer.displayName = 'PageContainer'; + +export const PageContent = styled.div` + flex: 1 1 auto; + height: 100%; + position: relative; + overflow-y: hidden; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + margin-top: 62px; +`; + +PageContent.displayName = 'PageContent'; + +export const FlexPage = styled(EuiPage)` + flex: 1 0 0; +`; + +FlexPage.displayName = 'FlexPage'; + +export const PageHeader = styled.div` + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + display: flex; + user-select: none; + padding: 1rem 1rem 0rem 1rem; + width: 100vw; + position: fixed; +`; + +PageHeader.displayName = 'PageHeader'; + +export const FooterContainer = styled.div` + flex: 0; + bottom: 0; + color: #666; + left: 0; + position: fixed; + text-align: left; + user-select: none; + width: 100%; + background-color: #f5f7fa; + padding: 16px; + border-top: 1px solid #d3dae6; +`; + +FooterContainer.displayName = 'FooterContainer'; + +export const PaneScrollContainer = styled.div` + height: 100%; + overflow-y: scroll; + > div:last-child { + margin-bottom: 3rem; + } +`; + +PaneScrollContainer.displayName = 'PaneScrollContainer'; + +export const Pane = styled.div` + height: 100%; + overflow: hidden; + user-select: none; +`; + +Pane.displayName = 'Pane'; + +export const PaneHeader = styled.div` + display: flex; +`; + +PaneHeader.displayName = 'PaneHeader'; + +export const Pane1FlexContent = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + height: 100%; +`; + +Pane1FlexContent.displayName = 'Pane1FlexContent'; + +export const CountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +CountBadge.displayName = 'CountBadge'; + +export const Spacer = styled.span` + margin-left: 5px; +`; + +Spacer.displayName = 'Spacer'; + +export const Badge = (styled(EuiBadge)` + vertical-align: top; +` as unknown) as typeof EuiBadge; + +Badge.displayName = 'Badge'; + +export const MoreRowItems = styled(EuiIcon)` + margin-left: 5px; +`; + +MoreRowItems.displayName = 'MoreRowItems'; + +export const OverviewWrapper = styled(EuiFlexGroup)` + position: relative; + + .euiButtonIcon { + position: absolute; + right: ${props => props.theme.eui.euiSizeM}; + top: 6px; + z-index: 2; + } +`; + +OverviewWrapper.displayName = 'OverviewWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx b/x-pack/plugins/siem/public/components/page/manage_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx rename to x-pack/plugins/siem/public/components/page/manage_query.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx rename to x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx b/x-pack/plugins/siem/public/components/page/network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/index.tsx rename to x-pack/plugins/siem/public/components/page/network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx new file mode 100644 index 0000000000000..56b59ca97156f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; +import { DescriptionList } from '../../../../../common/utility_types'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; +import { networkModel } from '../../../../store'; +import { getEmptyTagValue } from '../../../empty_value'; + +import { + autonomousSystemRenderer, + dateRenderer, + hostIdRenderer, + hostNameRenderer, + locationRenderer, + reputationRenderer, + whoisRenderer, +} from '../../../field_renderers/field_renderers'; +import * as i18n from './translations'; +import { DescriptionListStyled, OverviewWrapper } from '../../index'; +import { Loader } from '../../../loader'; +import { Anomalies, NarrowDateRange } from '../../../ml/types'; +import { AnomalyScores } from '../../../ml/score/anomaly_scores'; +import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; +import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; +import { InspectButton, InspectButtonContainer } from '../../../inspect'; + +interface OwnProps { + data: IpOverviewData; + flowTarget: FlowTarget; + id: string; + ip: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + type: networkModel.NetworkType; + narrowDateRange: NarrowDateRange; +} + +export type IpOverviewProps = OwnProps; + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { + return ( + <EuiFlexItem key={key}> + <DescriptionListStyled listItems={descriptionList} /> + </EuiFlexItem> + ); +}; + +export const IpOverview = React.memo<IpOverviewProps>( + ({ + id, + ip, + data, + loading, + flowTarget, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); + const typeData: Overview = data[flowTarget]!; + const column: DescriptionList[] = [ + { + title: i18n.LOCATION, + description: locationRenderer( + [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], + data + ), + }, + { + title: i18n.AUTONOMOUS_SYSTEM, + description: typeData + ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) + : getEmptyTagValue(), + }, + ]; + + const firstColumn: DescriptionList[] = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + <AnomalyScores + anomalies={anomaliesData} + startDate={startDate} + endDate={endDate} + isLoading={isLoadingAnomaliesData} + narrowDateRange={narrowDateRange} + /> + ), + }, + ] + : column; + + const descriptionLists: Readonly<DescriptionList[][]> = [ + firstColumn, + [ + { + title: i18n.FIRST_SEEN, + description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), + }, + { + title: i18n.LAST_SEEN, + description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.HOST_ID, + description: typeData + ? hostIdRenderer({ host: data.host, ipFilter: ip }) + : getEmptyTagValue(), + }, + { + title: i18n.HOST_NAME, + description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), + }, + ], + [ + { title: i18n.WHOIS, description: whoisRenderer(ip) }, + { title: i18n.REPUTATION, description: reputationRenderer(ip) }, + ], + ]; + + return ( + <InspectButtonContainer> + <OverviewWrapper> + <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + <Loader + overlay + overlayBackground={ + darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor + } + size="xl" + /> + )} + </OverviewWrapper> + </InspectButtonContainer> + ); + } +); + +IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/mock.ts b/x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/mock.ts rename to x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/translations.ts b/x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/translations.ts rename to x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx rename to x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts b/x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts rename to x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/translations.ts b/x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/translations.ts rename to x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/users_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/users_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/users_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/users_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx b/x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx new file mode 100644 index 0000000000000..52c142ceff480 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; +import { + ID as OverviewHostQueryId, + OverviewHostQuery, +} from '../../../../containers/overview/overview_host'; +import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { getHostsUrl } from '../../../link_to'; +import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; +import { manageQuery } from '../../../page/manage_query'; +import { inputsModel } from '../../../../store/inputs'; +import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; + +export interface OwnProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewHostStatsManage = manageQuery(OverviewHostStats); +export type OverviewHostProps = OwnProps; + +const OverviewHostComponent: React.FC<OverviewHostProps> = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + <EuiButton href={getHostsUrl(urlSearch)}> + <FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" /> + </EuiButton> + ), + [urlSearch] + ); + return ( + <EuiFlexItem> + <InspectButtonContainer> + <EuiPanel> + <OverviewHostQuery + data-test-subj="overview-host-query" + endDate={endDate} + filterQuery={filterQuery} + sourceId="default" + startDate={startDate} + > + {({ overviewHost, loading, id, inspect, refetch }) => { + const hostEventsCount = getOverviewHostStats(overviewHost).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); + + return ( + <> + <HeaderSection + id={OverviewHostQueryId} + subtitle={ + !isEmpty(overviewHost) ? ( + <FormattedMessage + defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}" + id="xpack.siem.overview.overviewHost.hostsSubtitle" + values={{ + formattedHostEventsCount, + hostEventsCount, + }} + /> + ) : ( + <>{''}</> + ) + } + title={ + <FormattedMessage + id="xpack.siem.overview.hostsTitle" + defaultMessage="Host events" + /> + } + > + {hostPageButton} + </HeaderSection> + + <OverviewHostStatsManage + loading={loading} + data={overviewHost} + setQuery={setQuery} + id={id} + inspect={inspect} + refetch={refetch} + /> + </> + ); + }} + </OverviewHostQuery> + </EuiPanel> + </InspectButtonContainer> + </EuiFlexItem> + ); +}; + +export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx new file mode 100644 index 0000000000000..d649a0dd9e923 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; +import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { manageQuery } from '../../../page/manage_query'; +import { + ID as OverviewNetworkQueryId, + OverviewNetworkQuery, +} from '../../../../containers/overview/overview_network'; +import { inputsModel } from '../../../../store/inputs'; +import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; +import { getNetworkUrl } from '../../../link_to'; +import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; + +export interface OverviewNetworkProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); + +const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + <EuiButton href={getNetworkUrl(urlSearch)}> + <FormattedMessage id="xpack.siem.overview.networkAction" defaultMessage="View network" /> + </EuiButton> + ), + [urlSearch] + ); + return ( + <EuiFlexItem> + <InspectButtonContainer> + <EuiPanel> + <OverviewNetworkQuery + data-test-subj="overview-network-query" + endDate={endDate} + filterQuery={filterQuery} + sourceId="default" + startDate={startDate} + > + {({ overviewNetwork, loading, id, inspect, refetch }) => { + const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedNetworkEventsCount = numeral(networkEventsCount).format( + defaultNumberFormat + ); + + return ( + <> + <HeaderSection + id={OverviewNetworkQueryId} + subtitle={ + !isEmpty(overviewNetwork) ? ( + <FormattedMessage + defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}" + id="xpack.siem.overview.overviewNetwork.networkSubtitle" + values={{ + formattedNetworkEventsCount, + networkEventsCount, + }} + /> + ) : ( + <>{''}</> + ) + } + title={ + <FormattedMessage + id="xpack.siem.overview.networkTitle" + defaultMessage="Network events" + /> + } + > + {networkPageButton} + </HeaderSection> + + <OverviewNetworkStatsManage + loading={loading} + data={overviewNetwork} + id={id} + inspect={inspect} + setQuery={setQuery} + refetch={refetch} + /> + </> + ); + }} + </OverviewNetworkQuery> + </EuiPanel> + </InspectButtonContainer> + </EuiFlexItem> + ); +}; + +OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; + +export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx b/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx rename to x-pack/plugins/siem/public/components/page/overview/stat_value.tsx index cada0a9aff939..7615001eec9da 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx +++ b/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useUiSetting$ } from '../../../lib/kibana'; const ProgressContainer = styled.div` diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/types.ts b/x-pack/plugins/siem/public/components/page/overview/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/types.ts rename to x-pack/plugins/siem/public/components/page/overview/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/translations.ts b/x-pack/plugins/siem/public/components/page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/translations.ts rename to x-pack/plugins/siem/public/components/page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/index.tsx b/x-pack/plugins/siem/public/components/page_route/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/index.tsx rename to x-pack/plugins/siem/public/components/page_route/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/pageroute.test.tsx b/x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/pageroute.test.tsx rename to x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/pageroute.tsx b/x-pack/plugins/siem/public/components/page_route/pageroute.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/pageroute.tsx rename to x-pack/plugins/siem/public/components/page_route/pageroute.tsx diff --git a/x-pack/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 0000000000000..94dac6607ce21 --- /dev/null +++ b/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx @@ -0,0 +1,522 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { Direction } from '../../graphql/types'; + +import { BasicTableProps, PaginatedTable } from './index'; +import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +jest.mock('react', () => { + const r = jest.requireActual('react'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { ...r, memo: (x: any) => x }; +}); + +describe('Paginated Table Component', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + let loadPage: jest.Mock<number>; + let updateLimitPagination: jest.Mock<number>; + let updateActivePage: jest.Mock<number>; + beforeEach(() => { + loadPage = jest.fn(); + updateLimitPagination = jest.fn(); + updateActivePage = jest.fn(); + }); + + describe('rendering', () => { + test('it renders the default load more table', () => { + const wrapper = shallow( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the loading panel at the beginning ', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={-1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={[]} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect( + wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() + ).toBeTruthy(); + }); + + test('it renders the over loading panel after data has been in the table ', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); + }); + + test('it renders the correct amount of pages and starts at activePage: 0', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + const paginiationProps = wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .props(); + + const expectedPaginationProps = { + 'data-test-subj': 'numberedPagination', + pageCount: 10, + activePage: 0, + }; + expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); + }); + + test('it render popover to select new limit in table', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); + }); + + test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={[]} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + + test('It should render a sort icon if sorting is defined', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={sortedHosts} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); + }); + + test('Should display toast when user reaches end of results max', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls.length).toEqual(0); + }); + + test('Should show items per row if totalCount is greater than items', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={30} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); + }); + + test('Should hide items per row if totalCount is less than items', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={1} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + }); + + describe('Events', () => { + test('should call updateActivePage with 1 when clicking to the first page', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[0][0]).toEqual(1); + }); + + test('Should call updateActivePage with 0 when you pick a new limit', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[1][0]).toEqual(0); + }); + + test('should update the page when the activePage is changed from redux', () => { + const ourProps: BasicTableProps<unknown> = { + activePage: 3, + columns: getHostsColumns(), + headerCount: 1, + headerSupplement: <p>{'My test supplement.'}</p>, + headerTitle: 'Hosts', + headerTooltip: 'My test tooltip', + headerUnit: 'Test Unit', + itemsPerRow: rowItems, + limit: 1, + loading: false, + loadPage, + pageOfItems: mockData.Hosts.edges, + showMorePagesIndicator: true, + totalCount: 10, + updateActivePage, + updateLimitPagination: limit => updateLimitPagination({ limit }), + }; + + // enzyme does not allow us to pass props to child of HOC + // so we make a component to pass it the props context + // ComponentWithContext will pass the changed props to Component + // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ComponentWithContext = (props: BasicTableProps<any>) => { + return ( + <ThemeProvider theme={theme}> + <PaginatedTable {...props} /> + </ThemeProvider> + ); + }; + + const wrapper = mount(<ComponentWithContext {...ourProps} />); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(3); + wrapper.setProps({ activePage: 0 }); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(0); + }); + + test('Should call updateLimitPagination when you pick a new limit', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateLimitPagination).toBeCalled(); + }); + + test('Should call onChange when you choose a new sort in the table', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={sortedHosts} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + expect(mockOnChange).toBeCalled(); + expect(mockOnChange.mock.calls[0]).toEqual([ + { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.tsx new file mode 100644 index 0000000000000..a815ecd100518 --- /dev/null +++ b/x-pack/plugins/siem/public/components/paginated_table/index.tsx @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBasicTable, + EuiBasicTableProps, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastListToast as Toast, + EuiLoadingContent, + EuiPagination, + EuiPopover, + Direction, +} from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; +import styled from 'styled-components'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { AuthTableColumns } from '../page/hosts/authentications_table'; +import { HostsTableColumns } from '../page/hosts/hosts_table'; +import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; +import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; +import { + NetworkTopNFlowColumns, + NetworkTopNFlowColumnsIpDetails, +} from '../page/network/network_top_n_flow_table/columns'; +import { + NetworkTopCountriesColumns, + NetworkTopCountriesColumnsIpDetails, +} from '../page/network/network_top_countries_table/columns'; +import { TlsColumns } from '../page/network/tls_table/columns'; +import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; +import { UsersColumns } from '../page/network/users_table/columns'; +import { HeaderSection } from '../header_section'; +import { Loader } from '../loader'; +import { useStateToaster } from '../toasters'; + +import * as i18n from './translations'; +import { Panel } from '../panel'; +import { InspectButtonContainer } from '../inspect'; + +const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; + +export interface ItemsPerRow { + text: string; + numberOfRow: number; +} + +export interface SortingBasicTable { + field: string; + direction: Direction; + allowNeutralSort?: boolean; +} + +export interface Criteria { + page?: { index: number; size: number }; + sort?: SortingBasicTable; +} + +declare type HostsTableColumnsTest = [ + Columns<string>, + Columns<string>, + Columns<string>, + Columns<string> +]; + +declare type BasicTableColumns = + | AuthTableColumns + | HostsTableColumns + | HostsTableColumnsTest + | NetworkDnsColumns + | NetworkHttpColumns + | NetworkTopCountriesColumns + | NetworkTopCountriesColumnsIpDetails + | NetworkTopNFlowColumns + | NetworkTopNFlowColumnsIpDetails + | TlsColumns + | UncommonProcessTableColumns + | UsersColumns; + +declare type SiemTables = BasicTableProps<BasicTableColumns>; + +// Using telescoping templates to remove 'any' that was polluting downstream column type checks +export interface BasicTableProps<T> { + activePage: number; + columns: T; + dataTestSubj?: string; + headerCount: number; + headerSupplement?: React.ReactElement; + headerTitle: string | React.ReactElement; + headerTooltip?: string; + headerUnit: string | React.ReactElement; + id?: string; + itemsPerRow?: ItemsPerRow[]; + isInspect?: boolean; + limit: number; + loading: boolean; + loadPage: (activePage: number) => void; + onChange?: (criteria: Criteria) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pageOfItems: any[]; + showMorePagesIndicator: boolean; + sorting?: SortingBasicTable; + totalCount: number; + updateActivePage: (activePage: number) => void; + updateLimitPagination: (limit: number) => void; +} +type Func<T> = (arg: T) => string | number; + +export interface Columns<T, U = T> { + align?: string; + field?: string; + hideForMobile?: boolean; + isMobileHeader?: boolean; + name: string | React.ReactNode; + render?: (item: T, node: U) => React.ReactNode; + sortable?: boolean | Func<T>; + truncateText?: boolean; + width?: string; +} + +const PaginatedTableComponent: FC<SiemTables> = ({ + activePage, + columns, + dataTestSubj = DEFAULT_DATA_TEST_SUBJ, + headerCount, + headerSupplement, + headerTitle, + headerTooltip, + headerUnit, + id, + isInspect, + itemsPerRow, + limit, + loading, + loadPage, + onChange = noop, + pageOfItems, + showMorePagesIndicator, + sorting = null, + totalCount, + updateActivePage, + updateLimitPagination, +}) => { + const [myLoading, setMyLoading] = useState(loading); + const [myActivePage, setActivePage] = useState(activePage); + const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const pageCount = Math.ceil(totalCount / limit); + const dispatchToaster = useStateToaster()[1]; + + useEffect(() => { + setActivePage(activePage); + }, [activePage]); + + useEffect(() => { + if (headerCount >= 0 && loadingInitial) { + setLoadingInitial(false); + } + }, [loadingInitial, headerCount]); + + useEffect(() => { + setMyLoading(loading); + }, [loading]); + + const onButtonClick = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setPopoverOpen(false); + }; + + const goToPage = (newActivePage: number) => { + if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + const toast: Toast = { + id: 'PaginationWarningMsg', + title: headerTitle + i18n.TOAST_TITLE, + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 10000, + text: i18n.TOAST_TEXT, + }; + return dispatchToaster({ + type: 'addToaster', + toast, + }); + } + setActivePage(newActivePage); + loadPage(newActivePage); + updateActivePage(newActivePage); + }; + + const button = ( + <EuiButtonEmpty + size="xs" + color="text" + iconType="arrowDown" + iconSide="right" + onClick={onButtonClick} + > + {`${i18n.ROWS}: ${limit}`} + </EuiButtonEmpty> + ); + + const rowItems = + itemsPerRow && + itemsPerRow.map((item: ItemsPerRow) => ( + <EuiContextMenuItem + key={item.text} + icon={limit === item.numberOfRow ? 'check' : 'empty'} + onClick={() => { + closePopover(); + updateLimitPagination(item.numberOfRow); + updateActivePage(0); // reset results to first page + }} + > + {item.text} + </EuiContextMenuItem> + )); + const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; + + return ( + <InspectButtonContainer show={!loadingInitial}> + <Panel data-test-subj={`${dataTestSubj}-loading-${loading}`} loading={loading}> + <HeaderSection + id={id} + subtitle={ + !loadingInitial && + `${i18n.SHOWING}: ${headerCount >= 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` + } + title={headerTitle} + tooltip={headerTooltip} + > + {!loadingInitial && headerSupplement} + </HeaderSection> + + {loadingInitial ? ( + <EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} /> + ) : ( + <> + <BasicTable + columns={columns} + compressed + items={pageOfItems} + onChange={onChange} + sorting={ + sorting + ? { + sort: { + field: sorting.field, + direction: sorting.direction, + }, + } + : undefined + } + /> + <FooterAction> + <EuiFlexItem> + {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( + <EuiPopover + id="customizablePagination" + data-test-subj="loadingMoreSizeRowPopover" + button={button} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + <EuiContextMenuPanel items={rowItems} data-test-subj="loadingMorePickSizeRow" /> + </EuiPopover> + )} + </EuiFlexItem> + + <PaginationWrapper grow={false}> + <EuiPagination + data-test-subj="numberedPagination" + pageCount={pageCount} + activePage={myActivePage} + onPageClick={goToPage} + /> + </PaginationWrapper> + </FooterAction> + {(isInspect || myLoading) && ( + <Loader data-test-subj="loadingPanelPaginatedTable" overlay size="xl" /> + )} + </> + )} + </Panel> + </InspectButtonContainer> + ); +}; + +export const PaginatedTable = memo(PaginatedTableComponent); + +type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any +const BasicTable = styled(EuiBasicTable as BasicTableType)` + tbody { + th, + td { + vertical-align: top; + } + + .euiTableCellContent { + display: block; + } + } +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +BasicTable.displayName = 'BasicTable'; + +const FooterAction = styled(EuiFlexGroup).attrs(() => ({ + alignItems: 'center', + responsive: false, +}))` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +FooterAction.displayName = 'FooterAction'; + +const PaginationEuiFlexItem = styled(EuiFlexItem)` + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + .euiButtonIcon:last-child { + margin-left: 28px; + } + + .euiPagination { + position: relative; + } + + .euiPagination::before { + bottom: 0; + color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; + content: '\\2026'; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + padding: 5px ${({ theme }) => theme.eui.euiSizeS}; + position: absolute; + right: ${({ theme }) => theme.eui.euiSizeL}; + } + } +`; + +PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts b/x-pack/plugins/siem/public/components/paginated_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts rename to x-pack/plugins/siem/public/components/paginated_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/panel/index.test.tsx b/x-pack/plugins/siem/public/components/panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/panel/index.test.tsx rename to x-pack/plugins/siem/public/components/panel/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/panel/index.tsx b/x-pack/plugins/siem/public/components/panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/panel/index.tsx rename to x-pack/plugins/siem/public/components/panel/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/pin/index.test.tsx b/x-pack/plugins/siem/public/components/pin/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/pin/index.test.tsx rename to x-pack/plugins/siem/public/components/pin/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/pin/index.tsx b/x-pack/plugins/siem/public/components/pin/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/pin/index.tsx rename to x-pack/plugins/siem/public/components/pin/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx b/x-pack/plugins/siem/public/components/port/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/index.test.tsx rename to x-pack/plugins/siem/public/components/port/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.tsx b/x-pack/plugins/siem/public/components/port/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/index.tsx rename to x-pack/plugins/siem/public/components/port/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx b/x-pack/plugins/siem/public/components/progress_inline/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx rename to x-pack/plugins/siem/public/components/progress_inline/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.tsx b/x-pack/plugins/siem/public/components/progress_inline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/index.tsx rename to x-pack/plugins/siem/public/components/progress_inline/index.tsx diff --git a/x-pack/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/query_bar/index.test.tsx new file mode 100644 index 0000000000000..e27669b2b15be --- /dev/null +++ b/x-pack/plugins/siem/public/components/query_bar/index.test.tsx @@ -0,0 +1,338 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; +import { TestProviders, mockIndexPattern } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager, SearchBar } from '../../../../../../src/plugins/data/public'; +import { QueryBar, QueryBarComponentProps } from '.'; +import { createKibanaContextProviderMock } from '../../mock/kibana_react'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +describe('QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockOnChangeQuery = jest.fn(); + const mockOnSubmitQuery = jest.fn(); + const mockOnSavedQuery = jest.fn(); + + beforeEach(() => { + mockOnChangeQuery.mockClear(); + mockOnSubmitQuery.mockClear(); + mockOnSavedQuery.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + <TestProviders> + <QueryBar + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + </TestProviders> + ); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + + expect(searchBarProps).toEqual({ + dataTestSubj: undefined, + dateRangeFrom: 'now-24h', + dateRangeTo: 'now', + filters: [], + indexPatterns: [ + { + fields: [ + { + aggregatable: true, + name: '@timestamp', + searchable: true, + type: 'date', + }, + { + aggregatable: true, + name: '@version', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test2', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test3', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test4', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test5', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test6', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test7', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test8', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'host.name', + searchable: true, + type: 'string', + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + ], + isLoading: false, + isRefreshPaused: true, + query: { + language: 'kuery', + query: 'here: query', + }, + refreshInterval: undefined, + showAutoRefreshOnly: false, + showDatePicker: false, + showFilterBar: true, + showQueryBar: true, + showQueryInput: true, + showSaveQuery: true, + }); + }); + + describe('#onQueryChange', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const KibanaWithStorageProvider = createKibanaContextProviderMock(); + + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <KibanaWithStorageProvider services={{ storage: { get: jest.fn() } }}> + <QueryBar {...props} /> + </KibanaWithStorageProvider> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + queryInput.simulate('change', { target: { value: 'hello: world' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onQuerySubmit', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSubmitQuery: jest.fn() }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onSavedQueryUpdated', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSavedQuery: jest.fn() }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/query_bar/index.tsx b/x-pack/plugins/siem/public/components/query_bar/index.tsx new file mode 100644 index 0000000000000..1ad7bc16b901e --- /dev/null +++ b/x-pack/plugins/siem/public/components/query_bar/index.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + FilterManager, + Query, + TimeHistory, + TimeRange, + SavedQuery, + SearchBar, + SavedQueryTimeFilter, +} from '../../../../../../src/plugins/data/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +export interface QueryBarComponentProps { + dataTestSubj?: string; + dateRangeFrom?: string; + dateRangeTo?: string; + hideSavedQuery?: boolean; + indexPattern: IIndexPattern; + isLoading?: boolean; + isRefreshPaused?: boolean; + filterQuery: Query; + filterManager: FilterManager; + filters: Filter[]; + onChangedQuery: (query: Query) => void; + onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; + refreshInterval?: number; + savedQuery?: SavedQuery | null; + onSavedQuery: (savedQuery: SavedQuery | null) => void; +} + +export const QueryBar = memo<QueryBarComponentProps>( + ({ + dateRangeFrom, + dateRangeTo, + hideSavedQuery = false, + indexPattern, + isLoading = false, + isRefreshPaused, + filterQuery, + filterManager, + filters, + onChangedQuery, + onSubmitQuery, + refreshInterval, + savedQuery, + onSavedQuery, + dataTestSubj, + }) => { + const [draftQuery, setDraftQuery] = useState(filterQuery); + + useEffect(() => { + setDraftQuery(filterQuery); + }, [filterQuery]); + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { + onSubmitQuery(payload.query); + } + }, + [filterQuery, onSubmitQuery] + ); + + const onQueryChange = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, draftQuery)) { + setDraftQuery(payload.query); + onChangedQuery(payload.query); + } + }, + [draftQuery, onChangedQuery, setDraftQuery] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + onSavedQuery(newSavedQuery); + }, + [onSavedQuery] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; + onSubmitQuery(newQuery, timefilter); + filterManager.setFilters(newFilters || []); + onSavedQuery(savedQueryUpdated); + }, + [filterManager, onSubmitQuery, onSavedQuery] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + onSubmitQuery({ + query: '', + language: savedQuery.attributes.query.language, + }); + filterManager.setFilters([]); + onSavedQuery(null); + } + }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); + + const onFiltersUpdated = useCallback( + (newFilters: Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + + const CustomButton = <>{null}</>; + const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); + + const searchBarProps = savedQuery != null ? { savedQuery } : {}; + + return ( + <SearchBar + customSubmitButton={CustomButton} + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + filters={filters} + indexPatterns={indexPatterns} + isLoading={isLoading} + isRefreshPaused={isRefreshPaused} + query={draftQuery} + onClearSavedQuery={onClearSavedQuery} + onFiltersUpdated={onFiltersUpdated} + onQueryChange={onQueryChange} + onQuerySubmit={onQuerySubmit} + onSaved={onSaved} + onSavedQueryUpdated={onSavedQueryUpdated} + refreshInterval={refreshInterval} + showAutoRefreshOnly={false} + showFilterBar={!hideSavedQuery} + showDatePicker={false} + showQueryBar={true} + showQueryInput={true} + showSaveQuery={true} + timeHistory={new TimeHistory(new Storage(localStorage))} + dataTestSubj={dataTestSubj} + {...searchBarProps} + /> + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx rename to x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts b/x-pack/plugins/siem/public/components/recent_cases/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts rename to x-pack/plugins/siem/public/components/recent_cases/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts b/x-pack/plugins/siem/public/components/recent_cases/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts rename to x-pack/plugins/siem/public/components/recent_cases/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx new file mode 100644 index 0000000000000..b641038f35ba6 --- /dev/null +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ApolloClient from 'apollo-client'; +import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { useGetAllTimeline } from '../../containers/timeline/all'; +import { SortFieldTimeline, Direction } from '../../graphql/types'; +import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; +import { OnOpenTimeline } from '../open_timeline/types'; +import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; + +import { RecentTimelines } from './recent_timelines'; +import * as i18n from './translations'; +import { FilterMode } from './types'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { navTabs } from '../../pages/home/home_navigations'; +import { getTimelinesUrl } from '../link_to/redirect_to_timelines'; + +interface OwnProps { + apolloClient: ApolloClient<{}>; + filterBy: FilterMode; +} + +export type Props = OwnProps & PropsFromRedux; + +const PAGE_SIZE = 3; + +const StatefulRecentTimelinesComponent = React.memo<Props>( + ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { + const onOpenTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + const noTimelinesMessage = + filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => <EuiLink href={getTimelinesUrl(urlSearch)}>{i18n.VIEW_ALL_TIMELINES}</EuiLink>, + [urlSearch] + ); + const loadingPlaceholders = useMemo( + () => ( + <LoadingPlaceholders lines={2} placeholders={filterBy === 'favorites' ? 1 : PAGE_SIZE} /> + ), + [filterBy] + ); + + const { fetchAllTimeline, timelines, totalCount, loading } = useGetAllTimeline(); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize: PAGE_SIZE, + }, + search: '', + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: filterBy === 'favorites', + timelines, + totalCount, + }); + }, [filterBy, timelines, totalCount]); + + return ( + <> + {loading ? ( + loadingPlaceholders + ) : ( + <RecentTimelines + noTimelinesMessage={noTimelinesMessage} + onOpenTimeline={onOpenTimeline} + timelines={timelines} + /> + )} + <EuiHorizontalRule margin="s" /> + <EuiText size="xs">{linkAllTimelines}</EuiText> + </> + ); + } +); + +StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts b/x-pack/plugins/siem/public/components/recent_timelines/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts rename to x-pack/plugins/siem/public/components/recent_timelines/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts b/x-pack/plugins/siem/public/components/recent_timelines/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts rename to x-pack/plugins/siem/public/components/recent_timelines/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx b/x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx rename to x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx b/x-pack/plugins/siem/public/components/scroll_to_top/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx rename to x-pack/plugins/siem/public/components/scroll_to_top/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/plugins/siem/public/components/search_bar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx rename to x-pack/plugins/siem/public/components/search_bar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts b/x-pack/plugins/siem/public/components/search_bar/selectors.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts rename to x-pack/plugins/siem/public/components/search_bar/selectors.ts index 793737a1ad754..4e700a46ca0e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts +++ b/x-pack/plugins/siem/public/components/search_bar/selectors.ts @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import { InputsRange } from '../../store/inputs/model'; -import { Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; +import { Query, SavedQuery } from '../../../../../../src/plugins/data/public'; export { endSelector, diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx b/x-pack/plugins/siem/public/components/selectable_text/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx rename to x-pack/plugins/siem/public/components/selectable_text/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.tsx b/x-pack/plugins/siem/public/components/selectable_text/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/index.tsx rename to x-pack/plugins/siem/public/components/selectable_text/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx b/x-pack/plugins/siem/public/components/sidebar_header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx rename to x-pack/plugins/siem/public/components/sidebar_header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx rename to x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/plugins/siem/public/components/skeleton_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx rename to x-pack/plugins/siem/public/components/skeleton_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx b/x-pack/plugins/siem/public/components/source_destination/country_flag.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx rename to x-pack/plugins/siem/public/components/source_destination/country_flag.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/field_names.ts b/x-pack/plugins/siem/public/components/source_destination/field_names.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/field_names.ts rename to x-pack/plugins/siem/public/components/source_destination/field_names.ts diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx b/x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx rename to x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/plugins/siem/public/components/source_destination/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx rename to x-pack/plugins/siem/public/components/source_destination/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx b/x-pack/plugins/siem/public/components/source_destination/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx rename to x-pack/plugins/siem/public/components/source_destination/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx b/x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx rename to x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/label.tsx b/x-pack/plugins/siem/public/components/source_destination/label.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/label.tsx rename to x-pack/plugins/siem/public/components/source_destination/label.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx b/x-pack/plugins/siem/public/components/source_destination/network.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx rename to x-pack/plugins/siem/public/components/source_destination/network.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/translations.ts b/x-pack/plugins/siem/public/components/source_destination/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/translations.ts rename to x-pack/plugins/siem/public/components/source_destination/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/types.ts b/x-pack/plugins/siem/public/components/source_destination/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/types.ts rename to x-pack/plugins/siem/public/components/source_destination/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx rename to x-pack/plugins/siem/public/components/stat_items/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx rename to x-pack/plugins/siem/public/components/stat_items/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx b/x-pack/plugins/siem/public/components/subtitle/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx rename to x-pack/plugins/siem/public/components/subtitle/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.tsx b/x-pack/plugins/siem/public/components/subtitle/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/index.tsx rename to x-pack/plugins/siem/public/components/subtitle/index.tsx diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx new file mode 100644 index 0000000000000..b6b515ceeffa6 --- /dev/null +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx @@ -0,0 +1,443 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; +import { apolloClientObservable, mockGlobalState } from '../../mock'; +import { createUseUiSetting$Mock } from '../../mock/kibana_react'; +import { createStore, State } from '../../store'; + +import { SuperDatePicker, makeMapStateToProps } from '.'; +import { cloneDeep } from 'lodash/fp'; + +jest.mock('../../lib/kibana'); +const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +const timepickerRanges = [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, +]; + +describe('SIEM Super Date Picker', () => { + describe('#SuperDatePicker', () => { + const state: State = mockGlobalState; + let store = createStore(state, apolloClientObservable); + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(state, apolloClientObservable); + mockUseUiSetting$.mockImplementation((key, defaultValue) => { + const useUiSetting$Mock = createUseUiSetting$Mock(); + + return key === DEFAULT_TIMEPICKER_QUICK_RANGES + ? [timepickerRanges, jest.fn()] + : useUiSetting$Mock(key, defaultValue); + }); + }); + + describe('Pick Relative Date', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make Sure it is relative date', () => { + expect(store.getState().inputs.global.timerange.kind).toBe('relative'); + }); + + test('Make Sure it is last 24 hours date', () => { + expect(store.getState().inputs.global.timerange.fromStr).toBe('now-24h'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now'); + }); + + test('Make Sure it is Today date', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); + }); + + test('Make Sure to (end date) is superior than from (start date)', () => { + expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( + store.getState().inputs.global.timerange.from + ); + }); + }); + + describe('Recently used date ranges', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Today is in Recently used date ranges', () => { + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + + test('Today and Last 24 hours are in Recently used date ranges', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Last 24 hoursToday'); + }); + + test('Make sure that it does not add any duplicate if you click again on today', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + }); + + describe('Refresh Every', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + const wrapperFixedEuiFieldSearch = wrapper.find( + 'input[data-test-subj="superDatePickerRefreshIntervalInput"]' + ); + + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: '2' } }); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make sure the duration get updated to 2 minutes === 120000ms', () => { + expect(store.getState().inputs.global.policy.duration).toEqual(120000); + }); + + test('Make sure the stream live started', () => { + expect(store.getState().inputs.global.policy.kind).toBe('interval'); + }); + + test('Make sure we can stop the stream live', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(store.getState().inputs.global.policy.kind).toBe('manual'); + }); + }); + + describe('Pick Absolute Date', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerShowDatesButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerstartDatePopoverButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerAbsoluteTab"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.react-datepicker__navigation--previous') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('div.react-datepicker__day') + .at(1) + .simulate('click'); + wrapper.update(); + + wrapper + .find('button[data-test-subj="superDatePickerApplyTimeButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + }); + + describe('#makeMapStateToProps', () => { + test('it should return the same shallow references given the same input twice', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const props2 = mapStateToProps(state, { id: 'global' }); + Object.keys(props1).forEach(key => { + expect((props1 as Record<string, {}>)[key]).toBe((props2 as Record<string, {}>)[key]); + }); + }); + + test('it should not return the same reference if policy kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.kind = 'interval'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.policy).not.toBe(props2.policy); + }); + + test('it should not return the same reference if duration is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.duration = 99999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.duration).not.toBe(props2.duration); + }); + + test('it should not return the same reference if timerange kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.kind = 'absolute'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.kind).not.toBe(props2.kind); + }); + + test('it should not return the same reference if timerange from is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.from = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.start).not.toBe(props2.start); + }); + + test('it should not return the same reference if timerange to is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.to = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.end).not.toBe(props2.end); + }); + + test('it should not return the same reference of toStr if toStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.toStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.toStr).not.toBe(props2.toStr); + }); + + test('it should not return the same reference of fromStr if fromStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.fromStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.fromStr).not.toBe(props2.fromStr); + }); + + test('it should not return the same reference of isLoadingSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.isLoading).not.toBe(props2.isLoading); + }); + + test('it should not return the same reference of refetchSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.queries).not.toBe(props2.queries); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx new file mode 100644 index 0000000000000..ad38a7d61bcba --- /dev/null +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { + EuiSuperDatePicker, + OnRefreshChangeProps, + EuiSuperDatePickerRecentRange, + OnRefreshProps, + OnTimeChangeProps, +} from '@elastic/eui'; +import { getOr, take, isEmpty } from 'lodash/fp'; +import React, { useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; +import { inputsModel, State } from '../../store'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { + policySelector, + durationSelector, + kindSelector, + startSelector, + endSelector, + fromStrSelector, + toStrSelector, + isLoadingSelector, + queriesSelector, + kqlQuerySelector, +} from './selectors'; +import { InputsRange } from '../../store/inputs/model'; + +const MAX_RECENTLY_USED_RANGES = 9; + +interface Range { + from: string; + to: string; + display: string; +} + +interface UpdateReduxTime extends OnTimeChangeProps { + id: InputsModelId; + kql?: inputsModel.GlobalKqlQuery | undefined; + timelineId?: string; +} + +interface ReturnUpdateReduxTime { + kqlHaveBeenUpdated: boolean; +} + +export type DispatchUpdateReduxTime = ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime) => ReturnUpdateReduxTime; + +interface OwnProps { + disabled?: boolean; + id: InputsModelId; + timelineId?: string; +} + +export type SuperDatePickerProps = OwnProps & PropsFromRedux; + +export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>( + ({ + duration, + end, + fromStr, + id, + isLoading, + kind, + kqlQuery, + policy, + queries, + setDuration, + start, + startAutoReload, + stopAutoReload, + timelineId, + toStr, + updateReduxTime, + }) => { + const [isQuickSelection, setIsQuickSelection] = useState(true); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>( + [] + ); + const onRefresh = useCallback( + ({ start: newStart, end: newEnd }: OnRefreshProps): void => { + const { kqlHaveBeenUpdated } = updateReduxTime({ + end: newEnd, + id, + isInvalid: false, + isQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const currentStart = formatDate(newStart); + const currentEnd = isQuickSelection + ? formatDate(newEnd, { roundUp: true }) + : formatDate(newEnd); + if ( + !kqlHaveBeenUpdated && + (!isQuickSelection || (start === currentStart && end === currentEnd)) + ) { + refetchQuery(queries); + } + }, + [end, id, isQuickSelection, kqlQuery, start, timelineId] + ); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + if (duration !== refreshInterval) { + setDuration({ id, duration: refreshInterval }); + } + + if (isPaused && policy === 'interval') { + stopAutoReload({ id }); + } else if (!isPaused && policy === 'manual') { + startAutoReload({ id }); + } + + if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { + refetchQuery(queries); + } + }, + [id, isQuickSelection, duration, policy, toStr] + ); + + const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const onTimeChange = useCallback( + ({ + start: newStart, + end: newEnd, + isQuickSelection: newIsQuickSelection, + isInvalid, + }: OnTimeChangeProps) => { + if (!isInvalid) { + updateReduxTime({ + end: newEnd, + id, + isInvalid, + isQuickSelection: newIsQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const newRecentlyUsedRanges = [ + { start: newStart, end: newEnd }, + ...take( + MAX_RECENTLY_USED_RANGES, + recentlyUsedRanges.filter( + recentlyUsedRange => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + ), + ]; + + setRecentlyUsedRanges(newRecentlyUsedRanges); + setIsQuickSelection(newIsQuickSelection); + } + }, + [recentlyUsedRanges, kqlQuery] + ); + + const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); + const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + + const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = isEmpty(quickRanges) + ? [] + : quickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + + return ( + <EuiSuperDatePicker + commonlyUsedRanges={commonlyUsedRanges} + end={endDate} + isLoading={isLoading} + isPaused={policy === 'manual'} + onRefresh={onRefresh} + onRefreshChange={onRefreshChange} + onTimeChange={onTimeChange} + recentlyUsedRanges={recentlyUsedRanges} + refreshInterval={duration} + showUpdateButton={true} + start={startDate} + /> + ); + } +); + +export const formatDate = ( + date: string, + options?: { + roundUp?: boolean; + } +) => { + const momentDate = dateMath.parse(date, options); + return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; +}; + +export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime): ReturnUpdateReduxTime => { + const fromDate = formatDate(start); + let toDate = formatDate(end, { roundUp: true }); + if (isQuickSelection) { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + toDate = formatDate(end); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + from: formatDate(start), + to: formatDate(end), + }) + ); + } + if (timelineId != null) { + dispatch( + timelineActions.updateRange({ + id: timelineId, + start: fromDate, + end: toDate, + }) + ); + } + if (kql) { + return { + kqlHaveBeenUpdated: kql.refetch(dispatch), + }; + } + + return { + kqlHaveBeenUpdated: false, + }; +}; + +export const makeMapStateToProps = () => { + const getDurationSelector = durationSelector(); + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getKindSelector = kindSelector(); + const getKqlQuerySelector = kqlQuerySelector(); + const getPolicySelector = policySelector(); + const getQueriesSelector = queriesSelector(); + const getStartSelector = startSelector(); + const getToStrSelector = toStrSelector(); + return (state: State, { id }: OwnProps) => { + const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { + duration: getDurationSelector(inputsRange), + end: getEndSelector(inputsRange), + fromStr: getFromStrSelector(inputsRange), + isLoading: getIsLoadingSelector(inputsRange), + kind: getKindSelector(inputsRange), + kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, + policy: getPolicySelector(inputsRange), + queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], + start: getStartSelector(inputsRange), + toStr: getToStrSelector(inputsRange), + }; + }; +}; + +SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + startAutoReload: ({ id }: { id: InputsModelId }) => + dispatch(inputsActions.startAutoReload({ id })), + stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), + setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => + dispatch(inputsActions.setDuration({ id, duration })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts b/x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts rename to x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts b/x-pack/plugins/siem/public/components/super_date_picker/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts rename to x-pack/plugins/siem/public/components/super_date_picker/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap rename to x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/plugins/siem/public/components/tables/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx rename to x-pack/plugins/siem/public/components/tables/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx b/x-pack/plugins/siem/public/components/tables/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx rename to x-pack/plugins/siem/public/components/tables/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx rename to x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/translations.ts b/x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/translations.ts rename to x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_id.ts b/x-pack/plugins/siem/public/components/timeline/body/column_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_id.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_id.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/constants.ts b/x-pack/plugins/siem/public/components/timeline/body/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/constants.ts rename to x-pack/plugins/siem/public/components/timeline/body/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts b/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts b/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts rename to x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/constants.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx similarity index 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 d05ab6bcc378f..66c559729cccd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -28,9 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -` as 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 31525a4904bc2..4cb8140e22cef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -19,9 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -` as 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 ddb07b4636b88..60c868f780ff3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -21,12 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -const BadgeHighlighted = styled(EuiBadge)` +const BadgeHighlighted = (styled(EuiBadge)` height: 20px; margin: 0 5px 0 5px; maxwidth: 85px; minwidth: 85px; -` as 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 a5525df5ef3c3..e04aed17c6d67 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -18,7 +18,7 @@ import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; import * as i18n from './translations'; -const ProviderBadgeStyled = styled(EuiBadge)` +const ProviderBadgeStyled = (styled(EuiBadge)` .euiToolTipAnchor { &::after { font-style: normal; @@ -44,7 +44,7 @@ const ProviderBadgeStyled = styled(EuiBadge)` margin-right: 0; margin-left: 4px; } -` as 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 0ae952412a973..3a691d2bbc621 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -54,9 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -const NumberProviderAndBadge = styled(EuiBadge)` +const NumberProviderAndBadge = (styled(EuiBadge)` margin: 0px 5px; -` as 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 a4de8ffa3b9c5..0c8a6932adf91 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { TestProviders } from '../../../mock/test_providers'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts b/x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts rename to x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/events.ts b/x-pack/plugins/siem/public/components/timeline/events.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/events.ts rename to x-pack/plugins/siem/public/components/timeline/events.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx b/x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx rename to x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/translations.tsx b/x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/translations.tsx rename to x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx b/x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/mock.ts b/x-pack/plugins/siem/public/components/timeline/footer/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/mock.ts rename to x-pack/plugins/siem/public/components/timeline/footer/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/translations.ts b/x-pack/plugins/siem/public/components/timeline/footer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/translations.ts rename to x-pack/plugins/siem/public/components/timeline/footer/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx new file mode 100644 index 0000000000000..6f2053488f69b --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { mockIndexPattern } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; +import { TestProviders } from '../../../mock/test_providers'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; + +import { TimelineHeader } from '.'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../lib/kibana'); + +describe('Header', () => { + const indexPattern = mockIndexPattern; + const mount = useMountAppended(); + + describe('rendering', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={false} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the data providers', () => { + const wrapper = mount( + <TestProviders> + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={false} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); + }); + + test('it renders the unauthorized call out providers', () => { + const wrapper = mount( + <TestProviders> + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={true} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx rename to x-pack/plugins/siem/public/components/timeline/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/translations.ts b/x-pack/plugins/siem/public/components/timeline/header/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/translations.ts rename to x-pack/plugins/siem/public/components/timeline/header/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx new file mode 100644 index 0000000000000..fc5a8ae924f82 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx @@ -0,0 +1,398 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash/fp'; +import { mockIndexPattern } from '../../mock'; + +import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { buildGlobalQuery, combineQueries } from './helpers'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { EsQueryConfig, Filter, esFilters } from '../../../../../../src/plugins/data/public'; + +const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); +const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + +describe('Build KQL Query', () => { + test('Build KQL query with one data provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); + }); + + test('Build KQL query with one data provider as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Buld KQL query with one data provider as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Buld KQL query with one data provider as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider', () => { + const dataProviders = mockDataProviders.slice(0, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); + }); + + test('Build KQL query with one data provider and one and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = mockDataProviders.slice(1, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); + }); + + test('Build KQL query with one data provider and one and as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider and multiple and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); +}); + +describe('Combined Queries', () => { + const config: EsQueryConfig = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: true, + dateFormatTZ: 'America/New_York', + }; + test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + }) + ).toBeNull(); + }); + + test('No Data Provider & No kqlQuery & isEventViewer is true', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + }); + }); + + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + + test('Only Data Provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with a date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only KQL search/filter query', () => { + const { filterQuery } = combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.tsx new file mode 100644 index 0000000000000..53ab7d81cadc2 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/helpers.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, isNumber, get } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; + +import { escapeQueryValue, convertToBuildEsQuery } from '../../lib/keury'; + +import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; +import { BrowserFields } from '../../containers/source'; +import { + IIndexPattern, + Query, + EsQueryConfig, + Filter, +} from '../../../../../../src/plugins/data/public'; + +const convertDateFieldToQuery = (field: string, value: string | number) => + `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; + +const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { + const baseFields = get('base', browserFields); + if (baseFields != null && baseFields.fields != null) { + return Object.keys(baseFields.fields); + } + return []; +}); + +const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { + const splitFields = field.split('.'); + const baseFields = getBaseFields(browserFields); + if (baseFields.includes(field)) { + return ['base', 'fields', field]; + } + return [splitFields[0], 'fields', field]; +}; + +const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + if (browserField != null && browserField.type === 'date') { + return true; + } + return false; +}; + +const buildQueryMatch = ( + dataProvider: DataProvider | DataProvidersAnd, + browserFields: BrowserFields +) => + `${dataProvider.excluded ? 'NOT ' : ''}${ + dataProvider.queryMatch.operator !== EXISTS_OPERATOR + ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) + ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) + : `${dataProvider.queryMatch.field} : ${ + isNumber(dataProvider.queryMatch.value) + ? dataProvider.queryMatch.value + : escapeQueryValue(dataProvider.queryMatch.value) + }` + : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` + }`.trim(); + +const buildQueryForAndProvider = ( + dataAndProviders: DataProvidersAnd[], + browserFields: BrowserFields +) => + dataAndProviders + .reduce((andQuery, andDataProvider) => { + const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; + return andDataProvider.enabled + ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` + : andQuery; + }, '') + .trim(); + +export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => + dataProviders + .reduce((query, dataProvider: DataProvider, i) => { + const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; + const openParen = i > 0 ? '(' : ''; + const closeParen = i > 0 ? ')' : ''; + return dataProvider.enabled + ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} + ${ + dataProvider.and.length > 0 + ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` + : '' + }${closeParen}`.trim() + : query; + }, '') + .trim(); + +export const combineQueries = ({ + config, + dataProviders, + indexPattern, + browserFields, + filters = [], + kqlQuery, + kqlMode, + start, + end, + isEventViewer, +}: { + config: EsQueryConfig; + dataProviders: DataProvider[]; + indexPattern: IIndexPattern; + browserFields: BrowserFields; + filters: Filter[]; + kqlQuery: Query; + kqlMode: string; + start: number; + end: number; + isEventViewer?: boolean; +}): { filterQuery: string } | null => { + const kuery: Query = { query: '', language: kqlQuery.language }; + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { + return null; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { + kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { + kuery.query = `(${buildGlobalQuery( + dataProviders, + browserFields + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } + const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; + const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; + kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( + kqlQuery.query as string + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; +}; + +/** + * The CSS class name of a "stateful event", which appears in both + * the `Timeline` and the `Events Viewer` widget + */ +export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/index.tsx rename to x-pack/plugins/siem/public/components/timeline/index.tsx diff --git a/x-pack/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 0000000000000..4c64c8a100b41 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiModal, + EuiOverlayMask, + EuiToolTip, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +import { Note } from '../../../lib/note'; +import { Notes } from '../../notes'; +import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { NOTES_PANEL_WIDTH } from './notes_size'; +import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; +import * as i18n from './translations'; +import { SiemPageName } from '../../../pages/home/types'; +import { timelineSelectors } from '../../../store/timeline'; +import { State } from '../../../store'; + +export const historyToolTip = 'The chronological history of actions related to this timeline'; +export const streamLiveToolTip = 'Update the Timeline as new data arrives'; +export const newTimelineToolTip = 'Create a new timeline'; + +const NotesCountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +NotesCountBadge.displayName = 'NotesCountBadge'; + +type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; +type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; +type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; + +export const StarIcon = React.memo<{ + isFavorite: boolean; + timelineId: string; + updateIsFavorite: UpdateIsFavorite; +}>(({ isFavorite, timelineId: id, updateIsFavorite }) => ( + // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener + // TODO: 2 error is: Elements with the 'button' interactive role must be focusable + // TODO: Investigate this error + // eslint-disable-next-line + <div role="button" onClick={() => updateIsFavorite({ id, isFavorite: !isFavorite })}> + {isFavorite ? ( + <EuiToolTip data-test-subj="timeline-favorite-filled-star-tool-tip" content={i18n.FAVORITE}> + <StyledStar data-test-subj="timeline-favorite-filled-star" type="starFilled" size="l" /> + </EuiToolTip> + ) : ( + <EuiToolTip content={i18n.NOT_A_FAVORITE}> + <StyledStar data-test-subj="timeline-favorite-empty-star" type="starEmpty" size="l" /> + </EuiToolTip> + )} + </div> +)); +StarIcon.displayName = 'StarIcon'; + +interface DescriptionProps { + description: string; + timelineId: string; + updateDescription: UpdateDescription; +} + +export const Description = React.memo<DescriptionProps>( + ({ description, timelineId, updateDescription }) => ( + <EuiToolTip data-test-subj="timeline-description-tool-tip" content={i18n.DESCRIPTION_TOOL_TIP}> + <DescriptionContainer data-test-subj="description-container"> + <EuiFieldText + aria-label={i18n.TIMELINE_DESCRIPTION} + data-test-subj="timeline-description" + fullWidth={true} + onChange={e => updateDescription({ id: timelineId, description: e.target.value })} + placeholder={i18n.DESCRIPTION} + spellCheck={true} + value={description} + /> + </DescriptionContainer> + </EuiToolTip> + ) +); +Description.displayName = 'Description'; + +interface NameProps { + timelineId: string; + title: string; + updateTitle: UpdateTitle; +} + +export const Name = React.memo<NameProps>(({ timelineId, title, updateTitle }) => ( + <EuiToolTip data-test-subj="timeline-title-tool-tip" content={i18n.TITLE}> + <NameField + aria-label={i18n.TIMELINE_TITLE} + data-test-subj="timeline-title" + onChange={e => updateTitle({ id: timelineId, title: e.target.value })} + placeholder={i18n.UNTITLED_TIMELINE} + spellCheck={true} + value={title} + /> + </EuiToolTip> +)); +Name.displayName = 'Name'; + +interface NewCaseProps { + onClosePopover: () => void; + timelineId: string; + timelineTitle: string; +} + +export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => { + const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + const handleClick = useCallback(() => { + onClosePopover(); + history.push({ + pathname: `/${SiemPageName.case}/create`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }, + }, + }); + }, [onClosePopover, history, timelineId, timelineTitle]); + + return ( + <EuiButtonEmpty + data-test-subj="attach-timeline-case" + color="text" + iconSide="left" + iconType="paperClip" + onClick={handleClick} + > + {i18n.ATTACH_TIMELINE_TO_NEW_CASE} + </EuiButtonEmpty> + ); +}); +NewCase.displayName = 'NewCase'; + +interface NewTimelineProps { + createTimeline: CreateTimeline; + onClosePopover: () => void; + timelineId: string; +} + +export const NewTimeline = React.memo<NewTimelineProps>( + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + <EuiButtonEmpty + data-test-subj="timeline-new" + color="text" + iconSide="left" + iconType="plusInCircle" + onClick={handleClick} + > + {i18n.NEW_TIMELINE} + </EuiButtonEmpty> + ); + } +); +NewTimeline.displayName = 'NewTimeline'; + +interface NotesButtonProps { + animate?: boolean; + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + size: 's' | 'l'; + showNotes: boolean; + toggleShowNotes: () => void; + text?: string; + toolTip?: string; + updateNote: UpdateNote; +} + +const getNewNoteId = (): string => uuid.v4(); + +interface LargeNotesButtonProps { + noteIds: string[]; + text?: string; + toggleShowNotes: () => void; +} + +const LargeNotesButton = React.memo<LargeNotesButtonProps>(({ noteIds, text, toggleShowNotes }) => ( + <EuiButton + data-test-subj="timeline-notes-button-large" + onClick={() => toggleShowNotes()} + size="m" + > + <EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiIcon color="subdued" size="m" type="editorComment" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {text && text.length ? <LabelText>{text}</LabelText> : null} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <NotesCountBadge data-test-subj="timeline-notes-count" color="hollow"> + {noteIds.length} + </NotesCountBadge> + </EuiFlexItem> + </EuiFlexGroup> + </EuiButton> +)); +LargeNotesButton.displayName = 'LargeNotesButton'; + +interface SmallNotesButtonProps { + noteIds: string[]; + toggleShowNotes: () => void; +} + +const SmallNotesButton = React.memo<SmallNotesButtonProps>(({ noteIds, toggleShowNotes }) => ( + <EuiButtonIcon + aria-label={i18n.NOTES} + data-test-subj="timeline-notes-button-small" + iconType="editorComment" + onClick={() => toggleShowNotes()} + /> +)); +SmallNotesButton.displayName = 'SmallNotesButton'; + +/** + * The internal implementation of the `NotesButton` + */ +const NotesButtonComponent = React.memo<NotesButtonProps>( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + text, + updateNote, + }) => ( + <ButtonContainer animate={animate} data-test-subj="timeline-notes-button-container"> + <> + {size === 'l' ? ( + <LargeNotesButton noteIds={noteIds} text={text} toggleShowNotes={toggleShowNotes} /> + ) : ( + <SmallNotesButton noteIds={noteIds} toggleShowNotes={toggleShowNotes} /> + )} + {size === 'l' && showNotes ? ( + <EuiOverlayMask> + <EuiModal maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes}> + <Notes + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + getNewNoteId={getNewNoteId} + updateNote={updateNote} + /> + </EuiModal> + </EuiOverlayMask> + ) : null} + </> + </ButtonContainer> + ) +); +NotesButtonComponent.displayName = 'NotesButtonComponent'; + +export const NotesButton = React.memo<NotesButtonProps>( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + toolTip, + text, + updateNote, + }) => + showNotes ? ( + <NotesButtonComponent + animate={animate} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + showNotes={showNotes} + size={size} + toggleShowNotes={toggleShowNotes} + text={text} + updateNote={updateNote} + /> + ) : ( + <EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip"> + <NotesButtonComponent + animate={animate} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + showNotes={showNotes} + size={size} + toggleShowNotes={toggleShowNotes} + text={text} + updateNote={updateNote} + /> + </EuiToolTip> + ) +); +NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/notes_size.ts b/x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/notes_size.ts rename to x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx b/x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/plugins/siem/public/components/timeline/properties/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/translations.ts b/x-pack/plugins/siem/public/components/timeline/properties/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/translations.ts rename to x-pack/plugins/siem/public/components/timeline/properties/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx new file mode 100644 index 0000000000000..a78e5b8e1d226 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -0,0 +1,409 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { mockBrowserFields } from '../../../containers/source/mock'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { mockIndexPattern, TestProviders } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; +import { QueryBar } from '../../query_bar'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { buildGlobalQuery } from '../helpers'; + +import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../lib/kibana'); + +describe('Timeline QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockApplyKqlFilterQuery = jest.fn(); + const mockSetFilters = jest.fn(); + const mockSetKqlFilterQueryDraft = jest.fn(); + const mockSetSavedQueryId = jest.fn(); + const mockUpdateReduxTime = jest.fn(); + + beforeEach(() => { + mockApplyKqlFilterQuery.mockClear(); + mockSetFilters.mockClear(); + mockSetKqlFilterQueryDraft.mockClear(); + mockSetSavedQueryId.mockClear(); + mockUpdateReduxTime.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + <TestProviders> + <QueryBarTimeline + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + </TestProviders> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + + expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); + expect(queryBarProps.dateRangeTo).toEqual('now'); + expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); + expect(queryBarProps.savedQuery).toEqual(null); + }); + + describe('#onChangeQuery', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSubmitQuery', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ timelineId: 'new-timeline' }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSavedQuery', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + + test('is only reference that changed when savedQueryId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ + savedQueryId: 'new', + }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + }); + + describe('#getDataProviderFilter', () => { + test('returns valid data provider filter with a simple bool data provider', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + name: 'Provider 1', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', + }, + }); + }); + + test('returns valid data provider filter with an exists operator', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery( + [ + { + id: `id-exists`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '', + operator: ':*', + }, + and: [], + }, + ], + mockBrowserFields + ), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx new file mode 100644 index 0000000000000..7d2b4f71183dd --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; +import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; + +import { + IIndexPattern, + Query, + Filter, + esFilters, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../utils/saved_query_services'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { QueryBar } from '../../query_bar'; +import { DataProvider } from '../data_providers/data_provider'; +import { buildGlobalQuery } from '../helpers'; + +export interface QueryBarTimelineComponentProps { + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filters: Filter[]; + filterManager: FilterManager; + filterQuery: KueryFilterQuery; + filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; + kqlMode: KqlMode; + indexPattern: IIndexPattern; + isRefreshPaused: boolean; + refreshInterval: number; + savedQueryId: string | null; + setFilters: (filters: Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + timelineId: string; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; +} + +const timelineFilterDropArea = 'timeline-filter-drop-area'; + +export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState<string>( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState<string>( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); + const [filterQueryConverted, setFilterQueryConverted] = useState<Query>({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState<Filter[]>([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState<string>( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters(filters); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + setFilters(filterWithoutDropArea); + setQueryBarFilters(filterWithoutDropArea); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + + useEffect(() => { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + if (!deepEqual(filters, filterWithoutDropArea)) { + filterManager.setFilters(filters); + } + }, [filters]); + + useEffect(() => { + setFilterQueryConverted({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + }, [filterQuery]); + + useEffect(() => { + setDataProvidersDsl( + convertKueryToElasticSearchQuery( + buildGlobalQuery(dataProviders, browserFields), + indexPattern + ) + ); + }, [dataProviders, browserFields, indexPattern]); + + useEffect(() => { + if (fromStr != null && toStr != null) { + setDateRangeFrom(fromStr); + setDateRangTo(toStr); + } else if (from != null && to != null) { + setDateRangeFrom(new Date(from).toISOString()); + setDateRangTo(new Date(to).toISOString()); + } + }, [from, fromStr, to, toStr]); + + useEffect(() => { + let isSubscribed = true; + async function setSavedQueryByServices() { + if (savedQueryId != null && savedQueryServices != null) { + try { + // The getSavedQuery function will throw a promise rejection in + // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts + // if the savedObjectsClient is undefined. This is happening in a test + // so I wrapped this in a try catch to keep the unhandled promise rejection + // warning from appearing in tests. + const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery({ + ...mySavedQuery, + attributes: { + ...mySavedQuery.attributes, + filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), + }, + }); + } + } catch (exc) { + setSavedQuery(null); + } + } else if (isSubscribed) { + setSavedQuery(null); + } + } + setSavedQueryByServices(); + return () => { + isSubscribed = false; + }; + }, [savedQueryId]); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + if ( + filterQueryDraft == null || + (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || + filterQueryDraft.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + } + }, + [filterQueryDraft] + ); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + if ( + filterQuery == null || + (filterQuery != null && filterQuery.expression !== newQuery.query) || + filterQuery.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); + } + if (timefilter != null) { + const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); + + updateReduxTime({ + id: 'timeline', + end: timefilter.to, + start: timefilter.from, + isInvalid: false, + isQuickSelection, + timelineId, + }); + } + }, + [filterQuery, timelineId] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + if (newSavedQuery.id !== savedQueryId) { + setSavedQueryId(newSavedQuery.id); + } + if (savedQueryServices != null && dataProvidersDsl !== '') { + const dataProviderFilterExists = + newSavedQuery.attributes.filters != null + ? newSavedQuery.attributes.filters.findIndex( + f => f.meta.controlledBy === timelineFilterDropArea + ) + : -1; + savedQueryServices.saveQuery( + { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [ + ...newSavedQuery.attributes.filters, + getDataProviderFilter(dataProvidersDsl), + ] + : [], + }, + { + overwrite: true, + } + ); + } + } else { + setSavedQueryId(null); + } + }, + [dataProvidersDsl, savedQueryId, savedQueryServices] + ); + + return ( + <QueryBar + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + hideSavedQuery={kqlMode === 'search'} + indexPattern={indexPattern} + isRefreshPaused={isRefreshPaused} + filterQuery={filterQueryConverted} + filterManager={filterManager} + filters={queryBarFilters} + onChangedQuery={onChangedQuery} + onSubmitQuery={onSubmitQuery} + refreshInterval={refreshInterval} + savedQuery={savedQuery} + onSavedQuery={onSavedQuery} + dataTestSubj={'timelineQueryInput'} + /> + ); + } +); + +export const getDataProviderFilter = (dataProviderDsl: string): Filter => { + const dslObject = JSON.parse(dataProviderDsl); + const key = Object.keys(dslObject); + return { + ...dslObject, + meta: { + alias: timelineFilterDropArea, + controlledBy: timelineFilterDropArea, + negate: false, + disabled: false, + type: 'custom', + key: isEmpty(key) ? 'bool' : key[0], + value: dataProviderDsl, + }, + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx new file mode 100644 index 0000000000000..fa92ef9ce5965 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; + +import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { + KueryFilterQuery, + SerializedFilterQuery, + State, + timelineSelectors, + inputsModel, + inputsSelectors, +} from '../../../store'; +import { timelineActions } from '../../../store/actions'; +import { KqlMode, TimelineModel, EventType } from '../../../store/timeline/model'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { dispatchUpdateReduxTime } from '../../super_date_picker'; +import { SearchOrFilter } from './search_or_filter'; + +interface OwnProps { + browserFields: BrowserFields; + filterManager: FilterManager; + indexPattern: IIndexPattern; + timelineId: string; +} + +type Props = OwnProps & PropsFromRedux; + +const StatefulSearchOrFilterComponent = React.memo<Props>( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + eventType, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + indexPattern, + isRefreshPaused, + kqlMode, + refreshInterval, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + timelineId, + to, + toStr, + updateEventType, + updateKqlMode, + updateReduxTime, + }) => { + const applyFilterQueryFromKueryExpression = useCallback( + (expression: string, kind) => + applyKqlFilterQuery({ + id: timelineId, + filterQuery: { + kuery: { + kind, + expression, + }, + serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), + }, + }), + [indexPattern, timelineId] + ); + + const setFilterQueryDraftFromKueryExpression = useCallback( + (expression: string, kind) => + setKqlFilterQueryDraft({ + id: timelineId, + filterQueryDraft: { + kind, + expression, + }, + }), + [timelineId] + ); + + const setFiltersInTimeline = useCallback( + (newFilters: Filter[]) => + setFilters({ + id: timelineId, + filters: newFilters, + }), + [timelineId] + ); + + const setSavedQueryInTimeline = useCallback( + (newSavedQueryId: string | null) => + setSavedQueryId({ + id: timelineId, + savedQueryId: newSavedQueryId, + }), + [timelineId] + ); + + const handleUpdateEventType = useCallback( + (newEventType: EventType) => + updateEventType({ + id: timelineId, + eventType: newEventType, + }), + [timelineId] + ); + + return ( + <SearchOrFilter + applyKqlFilterQuery={applyFilterQueryFromKueryExpression} + browserFields={browserFields} + dataProviders={dataProviders} + eventType={eventType} + filters={filters} + filterManager={filterManager} + filterQuery={filterQuery} + filterQueryDraft={filterQueryDraft} + from={from} + fromStr={fromStr} + indexPattern={indexPattern} + isRefreshPaused={isRefreshPaused} + kqlMode={kqlMode!} + refreshInterval={refreshInterval} + savedQueryId={savedQueryId} + setFilters={setFiltersInTimeline} + setKqlFilterQueryDraft={setFilterQueryDraftFromKueryExpression!} + setSavedQueryId={setSavedQueryInTimeline} + timelineId={timelineId} + to={to} + toStr={toStr} + updateEventType={handleUpdateEventType} + updateKqlMode={updateKqlMode!} + updateReduxTime={updateReduxTime} + /> + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.eventType === nextProps.eventType && + prevProps.filterManager === nextProps.filterManager && + prevProps.from === nextProps.from && + prevProps.fromStr === nextProps.fromStr && + prevProps.to === nextProps.to && + prevProps.toStr === nextProps.toStr && + prevProps.isRefreshPaused === nextProps.isRefreshPaused && + prevProps.refreshInterval === nextProps.refreshInterval && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filterQuery, nextProps.filterQuery) && + deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.kqlMode, nextProps.kqlMode) && + deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + deepEqual(prevProps.timelineId, nextProps.timelineId) + ); + } +); +StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); + const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; + const input: inputsModel.InputsRange = getInputsTimeline(state); + const policy: inputsModel.Policy = getInputsPolicy(state); + return { + dataProviders: timeline.dataProviders, + eventType: timeline.eventType ?? 'raw', + filterQuery: getKqlFilterQuery(state, timelineId)!, + filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, + filters: timeline.filters!, + from: input.timerange.from, + fromStr: input.timerange.fromStr!, + isRefreshPaused: policy.kind === 'manual', + kqlMode: getOr('filter', 'kqlMode', timeline), + refreshInterval: policy.duration, + savedQueryId: getOr(null, 'savedQueryId', timeline), + to: input.timerange.to, + toStr: input.timerange.toStr!, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id, + filterQuery, + }) + ), + updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => + dispatch(timelineActions.updateEventType({ id, eventType })), + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => + dispatch(timelineActions.updateKqlMode({ id, kqlMode })), + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { + id: string; + filterQueryDraft: KueryFilterQuery; + }) => + dispatch( + timelineActions.setKqlFilterQueryDraft({ + id, + filterQueryDraft, + }) + ), + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => + dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), + setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => + dispatch(timelineActions.setFilters({ id, filters })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 02a575db259bb..0b8ed71135744 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,11 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; +import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode, EventType } from '../../../store/timeline/model'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx rename to x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx new file mode 100644 index 0000000000000..4cc89e5bdba73 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -0,0 +1,280 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiSelectable, + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTextColor, + EuiSelectableOption, + EuiPortal, + EuiFilterGroup, + EuiFilterButton, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import { ListProps } from 'react-virtualized'; +import styled from 'styled-components'; + +import { useGetAllTimeline } from '../../../containers/timeline/all'; +import { SortFieldTimeline, Direction } from '../../../graphql/types'; +import { isUntitled } from '../../open_timeline/helpers'; +import * as i18nTimeline from '../../open_timeline/translations'; +import { OpenTimelineResult } from '../../open_timeline/types'; +import { getEmptyTagValue } from '../../empty_value'; + +import * as i18n from '../translations'; + +const MyEuiFlexItem = styled(EuiFlexItem)` + display: inline-block; + max-width: 296px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + padding 0px 4px; +`; + +const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` + .euiSelectable { + .euiFormControlLayout__childrenWrapper { + display: flex; + } + ${({ isLoading }) => `${ + isLoading + ? ` + .euiFormControlLayoutIcons { + display: none; + } + .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { + display: block; + left: 12px; + top: 12px; + }` + : '' + } + `} + } +`; + +const ORIGINAL_PAGE_SIZE = 50; +const POPOVER_HEIGHT = 260; +const TIMELINE_ITEM_HEIGHT = 50; + +export interface GetSelectableOptions { + timelines: OpenTimelineResult[]; + onlyFavorites: boolean; + searchTimelineValue: string; +} + +interface SelectableTimelineProps { + hideUntitled?: boolean; + getSelectableOptions: ({ + timelines, + onlyFavorites, + searchTimelineValue, + }: GetSelectableOptions) => EuiSelectableOption[]; + onClosePopover: () => void; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({ + hideUntitled = false, + getSelectableOptions, + onClosePopover, + onTimelineChange, +}) => { + const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); + const [heightTrigger, setHeightTrigger] = useState(0); + const [searchTimelineValue, setSearchTimelineValue] = useState(''); + const [onlyFavorites, setOnlyFavorites] = useState(false); + const [searchRef, setSearchRef] = useState<HTMLElement | null>(null); + const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); + + const onSearchTimeline = useCallback(val => { + setSearchTimelineValue(val); + }, []); + + const handleOnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + const handleOnScroll = useCallback( + ( + totalTimelines: number, + totalCount: number, + { + clientHeight, + scrollHeight, + scrollTop, + }: { + clientHeight: number; + scrollHeight: number; + scrollTop: number; + } + ) => { + if (totalTimelines < totalCount) { + const clientHeightTrigger = clientHeight * 1.2; + if ( + scrollTop > 10 && + scrollHeight - scrollTop < clientHeightTrigger && + scrollHeight > heightTrigger + ) { + setHeightTrigger(scrollHeight); + setPageSize(pageSize + ORIGINAL_PAGE_SIZE); + } + } + }, + [heightTrigger, pageSize] + ); + + const renderTimelineOption = useCallback((option, searchValue) => { + return ( + <EuiFlexGroup + gutterSize="s" + justifyContent="spaceBetween" + alignItems="center" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiFlexGroup gutterSize="none" direction="column"> + <MyEuiFlexItem data-test-subj="timeline" grow={false}> + <EuiHighlight search={searchValue}> + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} + </EuiHighlight> + </MyEuiFlexItem> + <MyEuiFlexItem grow={false}> + <EuiTextColor color="subdued" component="span"> + <small> + {option.description != null && option.description.trim().length > 0 + ? option.description + : getEmptyTagValue()} + </small> + </EuiTextColor> + </MyEuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiIcon + type={`${ + option.favorite != null && isEmpty(option.favorite) ? 'starEmpty' : 'starFilled' + }`} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); + }, []); + + const handleTimelineChange = useCallback( + options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' + ); + if (selectedTimeline != null && selectedTimeline.length > 0) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id + ); + } + onClosePopover(); + }, + [onClosePopover, onTimelineChange] + ); + + const favoritePortal = useMemo( + () => + searchRef != null ? ( + <EuiPortal insert={{ sibling: searchRef, position: 'after' }}> + <MyEuiFlexGroup gutterSize="xs" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiFilterGroup> + <EuiFilterButton + size="l" + data-test-subj="only-favorites-toggle" + hasActiveFilters={onlyFavorites} + onClick={handleOnToggleOnlyFavorites} + > + {i18nTimeline.ONLY_FAVORITES} + </EuiFilterButton> + </EuiFilterGroup> + </EuiFlexItem> + </MyEuiFlexGroup> + </EuiPortal> + ) : null, + [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] + ); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize, + }, + search: searchTimelineValue, + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount: timelineCount, + }); + }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); + + return ( + <EuiSelectableContainer isLoading={loading}> + <EuiSelectable + height={POPOVER_HEIGHT} + isLoading={loading && timelines.length === 0} + listProps={{ + rowHeight: TIMELINE_ITEM_HEIGHT, + showIcons: false, + virtualizedProps: ({ + onScroll: handleOnScroll.bind( + null, + timelines.filter(t => !hideUntitled || t.title !== '').length, + timelineCount + ), + } as unknown) as ListProps, + }} + renderOption={renderTimelineOption} + onChange={handleTimelineChange} + searchable + searchProps={{ + 'data-test-subj': 'timeline-super-select-search-box', + isLoading: loading, + placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, + onSearch: onSearchTimeline, + incremental: false, + inputRef: (ref: HTMLElement) => { + setSearchRef(ref); + }, + }} + singleSelection={true} + options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} + > + {(list, search) => ( + <> + {search} + {favoritePortal} + {list} + </> + )} + </EuiSelectable> + </EuiSelectableContainer> + ); +}; + +export const SelectableTimeline = memo(SelectableTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/plugins/siem/public/components/timeline/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/plugins/siem/public/components/timeline/timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/plugins/siem/public/components/timeline/timeline.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline.tsx index 222cc0530bddb..10f10b1a86f1e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/plugins/siem/public/components/timeline/timeline.tsx @@ -39,7 +39,7 @@ import { Filter, FilterManager, IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +} from '../../../../../../src/plugins/data/public'; const TimelineContainer = styled.div` height: 100%; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline_context.tsx index 7c1eadd8e8bed..25a0078b6066a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx @@ -6,7 +6,7 @@ import React, { createContext, memo, useContext, useEffect, useState } from 'react'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; import { TimelineAction } from './body/actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/plugins/siem/public/components/timeline/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/translations.ts rename to x-pack/plugins/siem/public/components/timeline/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap rename to x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/errors.ts b/x-pack/plugins/siem/public/components/toasters/errors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/errors.ts rename to x-pack/plugins/siem/public/components/toasters/errors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx b/x-pack/plugins/siem/public/components/toasters/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx rename to x-pack/plugins/siem/public/components/toasters/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx b/x-pack/plugins/siem/public/components/toasters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/index.tsx rename to x-pack/plugins/siem/public/components/toasters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx b/x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx rename to x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx b/x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx rename to x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/translations.ts b/x-pack/plugins/siem/public/components/toasters/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/translations.ts rename to x-pack/plugins/siem/public/components/toasters/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts b/x-pack/plugins/siem/public/components/toasters/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts rename to x-pack/plugins/siem/public/components/toasters/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/utils.ts b/x-pack/plugins/siem/public/components/toasters/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/utils.ts rename to x-pack/plugins/siem/public/components/toasters/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx b/x-pack/plugins/siem/public/components/top_n/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx rename to x-pack/plugins/siem/public/components/top_n/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts b/x-pack/plugins/siem/public/components/top_n/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts rename to x-pack/plugins/siem/public/components/top_n/helpers.ts diff --git a/x-pack/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/components/top_n/index.test.tsx new file mode 100644 index 0000000000000..9325dcf499b2b --- /dev/null +++ b/x-pack/plugins/siem/public/components/top_n/index.test.tsx @@ -0,0 +1,379 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../containers/source/mock'; +import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; +import { createStore, State } from '../../store'; +import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; + +import { Props } from './top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; + +jest.mock('../../lib/kibana'); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +const field = 'process.name'; +const value = 'nice'; + +const state: State = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + query: { + query: 'host.name : end*', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { + query: 'Linux', + }, + }, + query: { + match: { + 'host.os.name': { + query: 'Linux', + type: 'phrase', + }, + }, + }, + }, + ], + }, + timeline: { + ...mockGlobalState.inputs.timeline, + timerange: { + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: 1586835969047, + to: 1586922369047, + }, + }, + }, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [ACTIVE_TIMELINE_REDUX_ID]: { + ...mockGlobalState.timeline.timelineById.test, + id: ACTIVE_TIMELINE_REDUX_ID, + dataProviders: [ + { + id: + 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', + name: 'tcp', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'network.transport', + value: 'tcp', + operator: ':', + }, + and: [], + }, + ], + eventType: 'all', + filters: [ + { + meta: { + alias: null, + disabled: false, + key: 'source.port', + negate: false, + params: { + query: '30045', + }, + type: 'phrase', + }, + query: { + match: { + 'source.port': { + query: '30045', + type: 'phrase', + }, + }, + }, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'host.name : *', + }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { + kind: 'kuery', + expression: 'host.name : *', + }, + }, + }, + }, + }, +}; +const store = createStore(state, apolloClientObservable); + +describe('StatefulTopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering in a global NON-timeline context', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + <TestProviders store={store}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TestProviders> + ); + }); + + test('it has undefined combinedQueries when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toBeUndefined(); + }); + + test(`defaults to the 'Raw events' view when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('raw'); + }); + + test(`provides a 'deleteQuery' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeDefined(); + }); + + test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { query: 'Linux' }, + }, + query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, + }, + ]); + }); + + test(`provides 'from' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(0); + }); + + test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); + }); + + test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); + }); + + test(`provides 'to' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1); + }); + }); + + describe('rendering in a timeline context', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + + wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider value={{ id: ACTIVE_TIMELINE_REDUX_ID }}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + }); + + test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + ); + }); + + test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('all'); + }); + + test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeUndefined(); + }); + + test(`provides empty filters when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([]); + }); + + test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(1586835969047); + }); + + test('provides an empty query when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: '', language: 'kuery' }); + }); + + test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); + }); + + test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1586922369047); + }); + }); + + test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider + value={{ documentType: 'signals', id: ACTIVE_TIMELINE_REDUX_ID }} + > + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('signal'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/top_n/index.tsx b/x-pack/plugins/siem/public/components/top_n/index.tsx new file mode 100644 index 0000000000000..9863df42f101d --- /dev/null +++ b/x-pack/plugins/siem/public/components/top_n/index.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { GlobalTime } from '../../containers/global_time'; +import { BrowserFields, WithSource } from '../../containers/source'; +import { useKibana } from '../../lib/kibana'; +import { esQuery, Filter, Query } from '../../../../../../src/plugins/data/public'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { TimelineModel } from '../../store/timeline/model'; +import { combineQueries } from '../timeline/helpers'; +import { useTimelineTypeContext } from '../timeline/timeline_context'; + +import { getOptions } from './helpers'; +import { TopN } from './top_n'; + +/** The currently active timeline always has this Redux ID */ +export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; + +const EMPTY_FILTERS: Filter[] = []; +const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + + // The mapped Redux state provided to this component includes the global + // filters that appear at the top of most views in the app, and all the + // filters in the active timeline: + const mapStateToProps = (state: State) => { + const activeTimeline: TimelineModel = + getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; + const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); + + return { + activeTimelineEventType: activeTimeline.eventType, + activeTimelineFilters, + activeTimelineFrom: activeTimelineInput.timerange.from, + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineTo: activeTimelineInput.timerange.to, + dataProviders: activeTimeline.dataProviders, + globalQuery: getGlobalQuerySelector(state), + globalFilters: getGlobalFiltersQuerySelector(state), + kqlMode: activeTimeline.kqlMode, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +interface OwnProps { + browserFields: BrowserFields; + field: string; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} +type PropsFromRedux = ConnectedProps<typeof connector>; +type Props = OwnProps & PropsFromRedux; + +const StatefulTopNComponent: React.FC<Props> = ({ + activeTimelineEventType, + activeTimelineFilters, + activeTimelineFrom, + activeTimelineKqlQueryExpression, + activeTimelineTo, + browserFields, + dataProviders, + field, + globalFilters = EMPTY_FILTERS, + globalQuery = EMPTY_QUERY, + kqlMode, + onFilterAdded, + setAbsoluteRangeDatePicker, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + + // Regarding data from useTimelineTypeContext: + // * `documentType` (e.g. 'signals') may only be populated in some views, + // e.g. the `Signals` view on the `Detections` page. + // * `id` (`timelineId`) may only be populated when we are rendered in the + // context of the active timeline. + // * `indexToAdd`, which enables the signals index to be appended to + // the `indexPattern` returned by `WithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the signals index + // to the index pattern. + const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); + + const options = getOptions( + timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + ); + + return ( + <GlobalTime> + {({ from, deleteQuery, setQuery, to }) => ( + <WithSource sourceId="default" indexToAdd={indexToAdd}> + {({ indexPattern }) => ( + <TopN + combinedQueries={ + timelineId === ACTIVE_TIMELINE_REDUX_ID + ? combineQueries({ + browserFields, + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders, + end: activeTimelineTo, + filters: activeTimelineFilters, + indexPattern, + kqlMode, + kqlQuery: { + language: 'kuery', + query: activeTimelineKqlQueryExpression ?? '', + }, + start: activeTimelineFrom, + })?.filterQuery + : undefined + } + data-test-subj="top-n" + defaultView={ + documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value + } + deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} + field={field} + filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} + from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} + indexPattern={indexPattern} + indexToAdd={indexToAdd} + options={options} + query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget={ + timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' + } + setQuery={setQuery} + to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} + toggleTopN={toggleTopN} + onFilterAdded={onFilterAdded} + value={value} + /> + )} + </WithSource> + )} + </GlobalTime> + ); +}; + +StatefulTopNComponent.displayName = 'StatefulTopNComponent'; + +export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx b/x-pack/plugins/siem/public/components/top_n/top_n.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx rename to x-pack/plugins/siem/public/components/top_n/top_n.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx b/x-pack/plugins/siem/public/components/top_n/top_n.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx rename to x-pack/plugins/siem/public/components/top_n/top_n.tsx index 136252617e2a2..d8dc63ef92ec6 100644 --- a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx +++ b/x-pack/plugins/siem/public/components/top_n/top_n.tsx @@ -11,7 +11,7 @@ import { ActionCreator } from 'typescript-fsa'; import { EventsByDataset } from '../../pages/overview/events_by_dataset'; import { SignalsByCategory } from '../../pages/overview/signals_by_category'; -import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { EventType } from '../../store/timeline/model'; diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts b/x-pack/plugins/siem/public/components/top_n/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/translations.ts rename to x-pack/plugins/siem/public/components/top_n/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx rename to x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx b/x-pack/plugins/siem/public/components/truncatable_text/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx rename to x-pack/plugins/siem/public/components/truncatable_text/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/plugins/siem/public/components/url_state/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/constants.ts rename to x-pack/plugins/siem/public/components/url_state/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts b/x-pack/plugins/siem/public/components/url_state/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts rename to x-pack/plugins/siem/public/components/url_state/helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/url_state/helpers.ts b/x-pack/plugins/siem/public/components/url_state/helpers.ts new file mode 100644 index 0000000000000..62196b90e5e6f --- /dev/null +++ b/x-pack/plugins/siem/public/components/url_state/helpers.ts @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { parse, stringify } from 'query-string'; +import { decode, encode } from 'rison-node'; +import * as H from 'history'; + +import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; + +import { SiemPageName } from '../../pages/home/types'; +import { inputsSelectors, State, timelineSelectors } from '../../store'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { formatDate } from '../super_date_picker'; +import { NavTab } from '../navigation/types'; +import { CONSTANTS, UrlStateType } from './constants'; +import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; + +export const decodeRisonUrlState = <T>(value: string | undefined): T | null => { + try { + return value ? ((decode(value) as unknown) as T) : null; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return null; + } + throw error; + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const encodeRisonUrlState = (state: any) => encode(state); + +export const getQueryStringFromLocation = (search: string) => search.substring(1); + +export const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + + return Array.isArray(queryParam) ? queryParam[0] : queryParam; +}; + +export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => ( + queryString: string +): string => { + const previousQueryValues = parse(queryString, { sort: false }); + if (urlState == null || (typeof urlState === 'string' && urlState === '')) { + delete previousQueryValues[stateKey]; + + return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); + } + + // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ + // Remove this if these utilities are promoted to kibana core + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); +}; + +export const replaceQueryStringInLocation = ( + location: H.Location, + queryString: string +): H.Location => { + if (queryString === getQueryStringFromLocation(location.search)) { + return location; + } else { + return { + ...location, + search: `?${queryString}`, + }; + } +}; + +export const getUrlType = (pageName: string): UrlStateType => { + if (pageName === SiemPageName.overview) { + return 'overview'; + } else if (pageName === SiemPageName.hosts) { + return 'host'; + } else if (pageName === SiemPageName.network) { + return 'network'; + } else if (pageName === SiemPageName.detections) { + return 'detections'; + } else if (pageName === SiemPageName.timelines) { + return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; + } + return 'overview'; +}; + +export const getTitle = ( + pageName: string, + detailName: string | undefined, + navTabs: Record<string, NavTab> +): string => { + if (detailName != null) return detailName; + return navTabs[pageName] != null ? navTabs[pageName].name : ''; +}; + +export const makeMapStateToProps = () => { + const getInputsSelector = inputsSelectors.inputsSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); + const getTimelines = timelineSelectors.getTimelines(); + const mapStateToProps = (state: State) => { + const inputState = getInputsSelector(state); + const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; + const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; + + const timeline = Object.entries(getTimelines(state)).reduce( + (obj, [timelineId, timelineObj]) => ({ + id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', + isOpen: timelineObj.show, + }), + { id: '', isOpen: false } + ); + + let searchAttr: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + } = { + [CONSTANTS.appQuery]: getGlobalQuerySelector(state), + [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), + }; + const savedQuery = getGlobalSavedQuerySelector(state); + if (savedQuery != null && savedQuery.id !== '') { + searchAttr = { + [CONSTANTS.savedQuery]: savedQuery.id, + }; + } + + return { + urlState: { + ...searchAttr, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: globalTimerange, + linkTo: globalLinkTo, + }, + timeline: { + [CONSTANTS.timerange]: timelineTimerange, + linkTo: timelineLinkTo, + }, + }, + [CONSTANTS.timeline]: timeline, + }, + }; + }; + + return mapStateToProps; +}; + +export const updateTimerangeUrl = ( + timeRange: UrlInputsModel, + isInitializing: boolean +): UrlInputsModel => { + if (timeRange.global.timerange.kind === 'relative') { + timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); + } + if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + isInitializing, + history, + newUrlStateString, + pathName, + search, + updateTimerange, + urlKey, +}: UpdateUrlStateString): string => { + if (urlKey === CONSTANTS.appQuery) { + const queryState = decodeRisonUrlState<Query>(newUrlStateString); + if (queryState != null && queryState.query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { + const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.filters) { + const queryState = decodeRisonUrlState<Filter[]>(newUrlStateString); + if (isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timeline) { + const queryState = decodeRisonUrlState<TimelineUrl>(newUrlStateString); + if (queryState != null && queryState.id === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } + return search; +}; + +export const replaceStateInLocation = <T>({ + history, + urlStateToReplace, + urlStateKey, + pathName, + search, +}: ReplaceStateInLocation<T>) => { + const newLocation = replaceQueryStringInLocation( + { + hash: '', + pathname: pathName, + search, + state: '', + }, + replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) + ); + if (history) { + history.replace(newLocation); + } + return newLocation.search; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/plugins/siem/public/components/url_state/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx rename to x-pack/plugins/siem/public/components/url_state/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index.tsx rename to x-pack/plugins/siem/public/components/url_state/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx rename to x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx rename to x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx index 4838fb4499b87..54a196d1b8161 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx @@ -7,7 +7,7 @@ import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; import { inputsActions } from '../../store/actions'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; import { diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.test.ts b/x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.test.ts rename to x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.ts b/x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.ts rename to x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts rename to x-pack/plugins/siem/public/components/url_state/test_dependencies.ts index dc1b8d428bb20..974bee53bc2ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts @@ -15,7 +15,7 @@ import { HostsTableType } from '../../store/hosts/model'; import { CONSTANTS } from './constants'; import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; import { UrlStateContainerPropTypes, LocationTypes } from './types'; -import { Query } from '../../../../../../../src/plugins/data/public'; +import { Query } from '../../../../../../src/plugins/data/public'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/plugins/siem/public/components/url_state/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/types.ts rename to x-pack/plugins/siem/public/components/url_state/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/plugins/siem/public/components/url_state/use_url_state.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx rename to x-pack/plugins/siem/public/components/url_state/use_url_state.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts b/x-pack/plugins/siem/public/components/utility_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts rename to x-pack/plugins/siem/public/components/utility_bar/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx b/x-pack/plugins/siem/public/components/utility_bar/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx rename to x-pack/plugins/siem/public/components/utility_bar/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utils.ts b/x-pack/plugins/siem/public/components/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utils.ts rename to x-pack/plugins/siem/public/components/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx rename to x-pack/plugins/siem/public/components/with_hover_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx b/x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx rename to x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/plugins/siem/public/components/wrapper_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx rename to x-pack/plugins/siem/public/components/wrapper_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts rename to x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx new file mode 100644 index 0000000000000..2bbb4cde92b15 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; + +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; +import { AnomaliesQueryTabBodyProps } from './types'; +import { getAnomaliesFilterQuery } from './utils'; +import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +const ID = 'anomaliesOverTimeQuery'; + +export const AnomaliesQueryTabBody = ({ + deleteQuery, + endDate, + setQuery, + skip, + startDate, + type, + narrowDateRange, + filterQuery, + anomaliesFilterQuery, + AnomaliesTableComponent, + flowTarget, + ip, +}: AnomaliesQueryTabBodyProps) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const [, siemJobs] = useSiemJobs(true); + const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE); + + const mergedFilterQuery = getAnomaliesFilterQuery( + filterQuery, + anomaliesFilterQuery, + siemJobs, + anomalyScore, + flowTarget, + ip + ); + + return ( + <> + <MatrixHistogramContainer + endDate={endDate} + filterQuery={mergedFilterQuery} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={startDate} + type={type} + {...histogramConfigs} + /> + <AnomaliesTableComponent + startDate={startDate} + endDate={endDate} + skip={skip} + type={type as never} + narrowDateRange={narrowDateRange} + flowTarget={flowTarget} + ip={ip} + /> + </> + ); +}; + +AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts rename to x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts new file mode 100644 index 0000000000000..f6cae81e3c6c4 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { SetQuery } from '../../../pages/hosts/navigation/types'; +import { FlowTarget } from '../../../graphql/types'; +import { HostsType } from '../../../store/hosts/model'; +import { NetworkType } from '../../../store/network/model'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; + +interface QueryTabBodyProps { + type: HostsType | NetworkType; + filterQuery?: string | ESTermQuery; +} + +export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { + anomaliesFilterQuery?: object; + AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; + deleteQuery?: ({ id }: { id: string }) => void; + endDate: number; + flowTarget?: FlowTarget; + narrowDateRange: NarrowDateRange; + setQuery: SetQuery; + startDate: number; + skip: boolean; + updateDateRange?: UpdateDateRange; + hideHistogramIfEmpty?: boolean; + ip?: string; +}; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts new file mode 100644 index 0000000000000..790a797b2fead --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import deepmerge from 'deepmerge'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { createFilter } from '../../helpers'; +import { SiemJob } from '../../../components/ml_popover/types'; +import { FlowTarget } from '../../../graphql/types'; + +export const getAnomaliesFilterQuery = ( + filterQuery: string | ESTermQuery | undefined, + anomaliesFilterQuery: object = {}, + siemJobs: SiemJob[] = [], + anomalyScore: number, + flowTarget?: FlowTarget, + ip?: string +): string => { + const siemJobIds = siemJobs + .filter(job => job.isInstalled) + .map(job => job.id) + .map(jobId => ({ + match_phrase: { + job_id: jobId, + }, + })); + + const filterQueryString = createFilter(filterQuery); + const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; + const mergedFilterQuery = deepmerge.all([ + filterQueryObject, + anomaliesFilterQuery, + { + bool: { + filter: [ + { + bool: { + should: siemJobIds, + minimum_should_match: 1, + }, + }, + { + match_phrase: { + result_type: 'record', + }, + }, + flowTarget && + ip && { + match_phrase: { + [`${flowTarget}.ip`]: ip, + }, + }, + { + range: { + record_score: { + gte: anomalyScore, + }, + }, + }, + ], + }, + }, + ]); + + return JSON.stringify(mergedFilterQuery); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts b/x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/authentications/index.tsx b/x-pack/plugins/siem/public/containers/authentications/index.tsx new file mode 100644 index 0000000000000..6d4a88c45a768 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/authentications/index.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + AuthenticationsEdges, + GetAuthenticationsQuery, + PageInfoPaginated, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { authenticationsQuery } from './index.gql_query'; + +const ID = 'authenticationQuery'; + +export interface AuthenticationArgs { + authentications: AuthenticationsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: AuthenticationArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +export interface AuthenticationsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; +} + +type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; + +class AuthenticationsComponentQuery extends QueryTemplatePaginated< + AuthenticationsProps, + GetAuthenticationsQuery.Query, + GetAuthenticationsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetAuthenticationsQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }; + return ( + <Query<GetAuthenticationsQuery.Query, GetAuthenticationsQuery.Variables> + query={authenticationsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const authentications = getOr([], 'source.Authentications.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Authentications: { + ...fetchMoreResult.source.Authentications, + edges: [...fetchMoreResult.source.Authentications.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + authentications, + id, + inspect: getOr(null, 'source.Authentications.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Authentications.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Authentications.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getAuthenticationsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AuthenticationsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(AuthenticationsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts new file mode 100644 index 0000000000000..dcc31401564b1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + CaseUserActions, + FetchCasesProps, + SortFieldCase, +} from '../types'; +import { + actionLicenses, + allCases, + basicCase, + basicCaseCommentPatch, + basicCasePost, + casesStatus, + caseUserActions, + pushedCase, + respReporters, + serviceConnector, + tags, +} from '../mock'; +import { + CaseExternalServiceRequest, + CasePatchRequest, + CasePostRequest, + CommentRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + User, +} from '../../../../../case/common/api'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + return Promise.resolve(basicCase); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => + Promise.resolve(casesStatus); + +export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => + Promise.resolve(respReporters); + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => + Promise.resolve(basicCasePost); + +export const patchCase = async ( + caseId: string, + updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => Promise.resolve([basicCase]); + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => Promise.resolve(allCases.cases); + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCase); + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCaseCommentPatch); + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => + Promise.resolve(true); + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise<Case> => Promise.resolve(pushedCase); + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise<ServiceConnectorCaseResponse> => Promise.resolve(serviceConnector); + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => + Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/containers/case/api.test.tsx similarity index 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 693a7175ebc3e..ad61e2b46f6c5 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 0000000000000..b97f94a5a6b59 --- /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 0000000000000..c3611f490708a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CasesConfigurePatch, + CasesConfigureRequest, + Connector, +} from '../../../../../../case/common/api'; + +import { ApiProps } from '../../types'; +import { CaseConfigure } from '../types'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/plugins/siem/public/containers/case/configure/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts rename to x-pack/plugins/siem/public/containers/case/configure/api.test.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/api.ts b/x-pack/plugins/siem/public/containers/case/configure/api.ts new file mode 100644 index 0000000000000..4f516764e46f3 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/api.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { + Connector, + CasesConfigurePatch, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../case/common/api'; +import { KibanaServices } from '../../../lib/kibana'; + +import { + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, +} from '../../../../../case/common/constants'; + +import { ApiProps } from '../types'; +import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { CaseConfigure } from './types'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'GET', + signal, + } + ); + + return !isEmpty(response) + ? convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ) + : null; +}; + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'POST', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'PATCH', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/plugins/siem/public/containers/case/configure/mock.ts new file mode 100644 index 0000000000000..c6824bd50edb5 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/mock.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Connector, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../case/common/api'; +import { CaseConfigure, CasesConfigurationMapping } from './types'; + +export const mapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; +export const connectorsMock: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping, + }, + }, + isPreconfigured: false, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: false, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-pushing', + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connectorId: '123', + connectorName: 'My Connector', + closureType: 'close-by-pushing', + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/plugins/siem/public/containers/case/configure/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts rename to x-pack/plugins/siem/public/containers/case/configure/translations.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/types.ts b/x-pack/plugins/siem/public/containers/case/configure/types.ts new file mode 100644 index 0000000000000..ed95315c066dc --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticUser } from '../types'; +import { + ActionType, + CasesConfigurationMaps, + CaseField, + ClosureType, + Connector, + ThirdPartyField, +} from '../../../../../case/common/api'; + +export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField }; + +export interface CasesConfigurationMapping { + source: CaseField; + target: ThirdPartyField; + actionType: ActionType; +} + +export interface CaseConfigure { + createdAt: string; + createdBy: ElasticUser; + connectorId: string; + connectorName: string; + closureType: ClosureType; + updatedAt: string; + updatedBy: ElasticUser; + version: string; +} + +export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { + actionType?: ActionType; +} diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..2826e9a2c2e55 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { + initialState, + useCaseConfigure, + ReturnUseCaseConfigure, + ConnectorConfiguration, +} from './use_configure'; +import { mapping, caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +const configuration: ConnectorConfiguration = { + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + }, + version: caseConfigurationCamelCaseResponseMock.version, + firstLoad: true, + loading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('correctly sets mappings', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current.mapping).toEqual(null); + result.current.setMapping(mapping); + expect(result.current.mapping).toEqual(mapping); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.persistCaseConfigure(configuration); + expect(result.current.persistLoading).toBeTruthy(); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connectorId).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connectorId).toEqual('456'); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connectorId).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connectorId).toEqual('456'); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + ...initialState, + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + }, + firstLoad: true, + loading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx new file mode 100644 index 0000000000000..a185d435f7165 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -0,0 +1,326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useCallback, useReducer } from 'react'; +import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; + +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import * as i18n from './translations'; +import { CasesConfigurationMapping, ClosureType } from './types'; + +interface Connector { + connectorId: string; + connectorName: string; +} +export interface ConnectorConfiguration extends Connector { + closureType: ClosureType; +} + +export interface State extends ConnectorConfiguration { + currentConfiguration: ConnectorConfiguration; + firstLoad: boolean; + loading: boolean; + mapping: CasesConfigurationMapping[] | null; + persistLoading: boolean; + version: string; +} +export type Action = + | { + type: 'setCurrentConfiguration'; + currentConfiguration: ConnectorConfiguration; + } + | { + type: 'setConnector'; + connector: Connector; + } + | { + type: 'setLoading'; + payload: boolean; + } + | { + type: 'setFirstLoad'; + payload: boolean; + } + | { + type: 'setPersistLoading'; + payload: boolean; + } + | { + type: 'setVersion'; + payload: string; + } + | { + type: 'setClosureType'; + closureType: ClosureType; + } + | { + type: 'setMapping'; + mapping: CasesConfigurationMapping[]; + }; + +export const configureCasesReducer = (state: State, action: Action) => { + switch (action.type) { + case 'setLoading': + return { + ...state, + loading: action.payload, + }; + case 'setFirstLoad': + return { + ...state, + firstLoad: action.payload, + }; + case 'setPersistLoading': + return { + ...state, + persistLoading: action.payload, + }; + case 'setVersion': + return { + ...state, + version: action.payload, + }; + case 'setCurrentConfiguration': { + return { + ...state, + currentConfiguration: { ...action.currentConfiguration }, + }; + } + case 'setConnector': { + return { + ...state, + ...action.connector, + }; + } + case 'setClosureType': { + return { + ...state, + closureType: action.closureType, + }; + } + case 'setMapping': { + return { + ...state, + mapping: action.mapping, + }; + } + default: + return state; + } +}; + +export interface ReturnUseCaseConfigure extends State { + persistCaseConfigure: ({ + connectorId, + connectorName, + closureType, + }: ConnectorConfiguration) => unknown; + refetchCaseConfigure: () => void; + setClosureType: (closureType: ClosureType) => void; + setConnector: (connectorId: string, connectorName?: string) => void; + setCurrentConfiguration: (configuration: ConnectorConfiguration) => void; + setMapping: (newMapping: CasesConfigurationMapping[]) => void; +} + +export const initialState: State = { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + currentConfiguration: { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + }, + firstLoad: false, + loading: true, + mapping: null, + persistLoading: false, + version: '', +}; + +export const useCaseConfigure = (): ReturnUseCaseConfigure => { + const [state, dispatch] = useReducer(configureCasesReducer, initialState); + + const setCurrentConfiguration = useCallback((configuration: ConnectorConfiguration) => { + dispatch({ + currentConfiguration: configuration, + type: 'setCurrentConfiguration', + }); + }, []); + + const setConnector = useCallback((connectorId: string, connectorName?: string) => { + dispatch({ + connector: { connectorId, connectorName: connectorName ?? '' }, + type: 'setConnector', + }); + }, []); + + const setClosureType = useCallback((closureType: ClosureType) => { + dispatch({ + closureType, + type: 'setClosureType', + }); + }, []); + + const setMapping = useCallback((newMapping: CasesConfigurationMapping[]) => { + dispatch({ + mapping: newMapping, + type: 'setMapping', + }); + }, []); + + const setLoading = useCallback((isLoading: boolean) => { + dispatch({ + payload: isLoading, + type: 'setLoading', + }); + }, []); + + const setFirstLoad = useCallback((isFirstLoad: boolean) => { + dispatch({ + payload: isFirstLoad, + type: 'setFirstLoad', + }); + }, []); + + const setPersistLoading = useCallback((isPersistLoading: boolean) => { + dispatch({ + payload: isPersistLoading, + type: 'setPersistLoading', + }); + }, []); + + const setVersion = useCallback((version: string) => { + dispatch({ + payload: version, + type: 'setVersion', + }); + }, []); + + const [, dispatchToaster] = useStateToaster(); + + const refetchCaseConfigure = useCallback(() => { + let didCancel = false; + const abortCtrl = new AbortController(); + + const fetchCaseConfiguration = async () => { + try { + setLoading(true); + const res = await getCaseConfigure({ signal: abortCtrl.signal }); + if (!didCancel) { + if (res != null) { + setConnector(res.connectorId, res.connectorName); + if (setClosureType != null) { + setClosureType(res.closureType); + } + setVersion(res.version); + + if (!state.firstLoad) { + setFirstLoad(true); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connectorId: res.connectorId, + connectorName: res.connectorName, + }); + } + } + } + setLoading(false); + } + } catch (error) { + if (!didCancel) { + setLoading(false); + errorToToaster({ + dispatchToaster, + error: error.body && error.body.message ? new Error(error.body.message) : error, + title: i18n.ERROR_TITLE, + }); + } + } + }; + + fetchCaseConfiguration(); + + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, [state.firstLoad]); + + const persistCaseConfigure = useCallback( + async ({ connectorId, connectorName, closureType }: ConnectorConfiguration) => { + let didCancel = false; + const abortCtrl = new AbortController(); + const saveCaseConfiguration = async () => { + try { + setPersistLoading(true); + const connectorObj = { + connector_id: connectorId, + connector_name: connectorName, + closure_type: closureType, + }; + const res = + state.version.length === 0 + ? await postCaseConfigure(connectorObj, abortCtrl.signal) + : await patchCaseConfigure( + { + ...connectorObj, + version: state.version, + }, + abortCtrl.signal + ); + if (!didCancel) { + setConnector(res.connectorId, res.connectorName); + if (setClosureType) { + setClosureType(res.closureType); + } + setVersion(res.version); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + connectorId: res.connectorId, + closureType: res.closureType, + connectorName: res.connectorName, + }); + } + + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); + } + } catch (error) { + if (!didCancel) { + setPersistLoading(false); + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + } + }; + saveCaseConfiguration(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [state.version] + ); + + useEffect(() => { + refetchCaseConfigure(); + }, []); + + return { + ...state, + refetchCaseConfigure, + persistCaseConfigure, + setCurrentConfiguration, + setConnector, + setClosureType, + setMapping, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx rename to x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx rename to x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx diff --git a/x-pack/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 0000000000000..0f44b3a1594ba --- /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 0000000000000..dde13dc38aca8 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/types.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { User, UserActionField, UserAction } from '../../../../case/common/api'; + +export interface Comment { + id: string; + createdAt: string; + createdBy: ElasticUser; + comment: string; + pushedAt: string | null; + pushedBy: string | null; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +} +export interface CaseUserActions { + actionId: string; + actionField: UserActionField; + action: UserAction; + actionAt: string; + actionBy: ElasticUser; + caseId: string; + commentId: string | null; + newValue: string | null; + oldValue: string | null; +} + +export interface CaseExternalService { + pushedAt: string; + pushedBy: ElasticUser; + connectorId: string; + connectorName: string; + externalId: string; + externalTitle: string; + externalUrl: string; +} +export interface Case { + id: string; + closedAt: string | null; + closedBy: ElasticUser | null; + comments: Comment[]; + createdAt: string; + createdBy: ElasticUser; + description: string; + externalService: CaseExternalService | null; + status: string; + tags: string[]; + title: string; + totalComment: number; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: 'asc' | 'desc'; +} + +export interface FilterOptions { + search: string; + status: string; + tags: string[]; + reporters: User[]; +} + +export interface CasesStatus { + countClosedCases: number | null; + countOpenCases: number | null; +} + +export interface AllCases extends CasesStatus { + cases: Case[]; + page: number; + perPage: number; + total: number; +} + +export enum SortFieldCase { + createdAt = 'createdAt', + closedAt = 'closedAt', +} + +export interface ElasticUser { + readonly email?: string | null; + readonly fullName?: string | null; + readonly username?: string | null; +} + +export interface FetchCasesProps extends ApiProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} + +export interface ApiProps { + signal: AbortSignal; +} + +export interface BulkUpdateStatus { + status: string; + id: string; + version: string; +} +export interface ActionLicense { + id: string; + name: string; + enabled: boolean; + enabledInConfig: boolean; + enabledInLicense: boolean; +} + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx b/x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx rename to x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx b/x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx index 2fc9b8294c8e0..01679ae4ccd82 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from 'react'; import { isEmpty } from 'lodash/fp'; -import { User } from '../../../../../../plugins/case/common/api'; +import { User } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getReporters } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/plugins/siem/public/containers/case/use_get_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_tags.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/plugins/siem/public/containers/case/use_post_case.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_case.tsx index aeb50fc098eee..b33269f26e97d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_case.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CasePostRequest } from '../../../../../../plugins/case/common/api'; +import { CasePostRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_comment.tsx index c6d34b5449977..c7d3b4125aada 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CommentRequest } from '../../../../../../plugins/case/common/api'; +import { CommentRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx similarity index 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 89e7e18cf0688..acd4b92ee430d 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 7ebbbba076c12..2f2fe18321246 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_update_case.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; -import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; +import { CasePatchRequest } from '../../../../case/common/api'; import { patchCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx b/x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/plugins/siem/public/containers/case/use_update_comment.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_comment.tsx diff --git a/x-pack/plugins/siem/public/containers/case/utils.ts b/x-pack/plugins/siem/public/containers/case/utils.ts new file mode 100644 index 0000000000000..aaa5ff4ab44c1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/utils.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { camelCase, isArray, isObject, set } from 'lodash'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + CasesFindResponse, + CasesFindResponseRt, + CaseResponse, + CaseResponseRt, + CasesResponse, + CasesResponseRt, + CasesStatusResponseRt, + CasesStatusResponse, + throwErrors, + CasesConfigureResponse, + CaseConfigureResponseRt, + CaseUserActionsResponse, + CaseUserActionsResponseRt, + ServiceConnectorCaseResponseRt, + ServiceConnectorCaseResponse, +} from '../../../../case/common/api'; +import { ToasterError } from '../../components/toasters'; +import { AllCases, Case } from './types'; + +export const getTypedPayload = <T>(a: unknown): T => a as T; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U => + Object.entries(snakeCase).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ + cases: snakeCases.cases.map(snakeCase => convertToCamelCase<CaseResponse, Case>(snakeCase)), + countClosedCases: snakeCases.count_closed_cases, + countOpenCases: snakeCases.count_open_cases, + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); + +export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => + pipe( + CasesStatusResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const createToasterPlainError = (message: string) => new ToasterError([message]); + +export const decodeCaseResponse = (respCase?: CaseResponse) => + pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesResponse = (respCase?: CasesResponse) => + pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => + pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => + pipe( + CaseConfigureResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => + pipe( + CaseUserActionsResponseRt.decode(respUserActions), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => + pipe( + ServiceConnectorCaseResponseRt.decode(respPushCase), + fold(throwErrors(createToasterPlainError), identity) + ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts new file mode 100644 index 0000000000000..c1fadf289ef4d --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -0,0 +1,331 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + DETECTION_ENGINE_TAGS_URL, +} from '../../../../common/constants'; +import { + AddRulesProps, + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + NewRule, + Rule, + FetchRuleProps, + BasicFetchProps, + ImportDataProps, + ExportDocumentsProps, + RuleStatusResponse, + ImportDataResponse, + PrePackagedRulesStatusResponse, + BulkRuleResponse, +} from './types'; +import { KibanaServices } from '../../../lib/kibana'; +import * as i18n from '../../../pages/detection_engine/rules/translations'; + +/** + * Add provided Rule + * + * @param rule to add + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => + KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { + method: rule.id != null ? 'PUT' : 'POST', + body: JSON.stringify(rule), + signal, + }); + +/** + * Fetches all rules from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + signal, +}: FetchRulesProps): Promise<FetchRulesResponse> => { + const filters = [ + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), + ...(filterOptions.showCustomRules + ? [`alert.attributes.tags: "__internal_immutable:false"`] + : []), + ...(filterOptions.showElasticRules + ? [`alert.attributes.tags: "__internal_immutable:true"`] + : []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), + ]; + + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; + + return KibanaServices.get().http.fetch<FetchRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_find`, + { + method: 'GET', + query, + signal, + } + ); +}; + +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => + KibanaServices.get().http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { + method: 'GET', + query: { id }, + signal, + }); + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * + * @throws An error if response is not OK + */ +export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<BulkRuleResponse>(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { + method: 'PATCH', + body: JSON.stringify(ids.map(id => ({ id, enabled }))), + }); + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * + * @throws An error if response is not OK + */ +export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { + method: 'DELETE', + body: JSON.stringify(ids.map(id => ({ id }))), + }); + +/** + * Duplicates provided Rules + * + * @param rules to duplicate + * + * @throws An error if response is not OK + */ +export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { + method: 'POST', + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + last_failure_at: undefined, + last_failure_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + }); + +/** + * Create Prepackaged Rules + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { + await KibanaServices.get().http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { + method: 'PUT', + signal, + }); + + return true; +}; + +/** + * Imports rules in the same format as exported via the _export API + * + * @param fileToImport File to upload containing rules to import + * @param overwrite whether or not to overwrite rules with the same ruleId + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const importRules = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise<ImportDataResponse> => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch<ImportDataResponse>( + `${DETECTION_ENGINE_RULES_URL}/_import`, + { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + } + ); +}; + +/** + * Export rules from the server as a file download + * + * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) + * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) + * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const exportRules = async ({ + excludeExportDetails = false, + filename = `${i18n.EXPORT_FILENAME}.ndjson`, + ids = [], + signal, +}: ExportDocumentsProps): Promise<Blob> => { + const body = + ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; + + return KibanaServices.get().http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + }); +}; + +/** + * Get Rule Status provided Rule ID + * + * @param id string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleStatusById = async ({ + id, + signal, +}: { + id: string; + signal: AbortSignal; +}): Promise<RuleStatusResponse> => + KibanaServices.get().http.fetch<RuleStatusResponse>(DETECTION_ENGINE_RULES_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ ids: [id] }), + signal, + }); + +/** + * Return rule statuses given list of alert ids + * + * @param ids array of string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRulesStatusByIds = async ({ + ids, + signal, +}: { + ids: string[]; + signal: AbortSignal; +}): Promise<RuleStatusResponse> => { + const res = await KibanaServices.get().http.fetch<RuleStatusResponse>( + DETECTION_ENGINE_RULES_STATUS_URL, + { + method: 'POST', + body: JSON.stringify({ ids }), + signal, + } + ); + return res; +}; + +/** + * Fetch all unique Tags used by Rules + * + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => + KibanaServices.get().http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { + method: 'GET', + signal, + }); + +/** + * Get pre packaged rules Status + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise<PrePackagedRulesStatusResponse> => + KibanaServices.get().http.fetch<PrePackagedRulesStatusResponse>( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + { + method: 'GET', + signal, + } + ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 83b8a3581a4be..8c688fe5615f0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -6,7 +6,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { useApolloClient } from '../../../utils/apollo_context'; import { mocksSource } from '../../source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index c5aefac15f48e..7e222045a1a3b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -8,7 +8,7 @@ import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import deepEqual from 'fast-deep-equal'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..f89d21ef1aeb1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { RuleTypeSchema } from '../../../../common/detection_engine/types'; + +/** + * Params is an "record", since it is a type of AlertActionParams which is action templates. + * @see x-pack/plugins/alerting/common/alert.ts + */ +export const action = t.exact( + t.type({ + group: t.string, + id: t.string, + action_type_id: t.string, + params: t.record(t.string, t.any), + }) +); + +export const NewRuleSchema = t.intersection([ + t.type({ + description: t.string, + enabled: t.boolean, + interval: t.string, + name: t.string, + risk_score: t.number, + severity: t.string, + type: RuleTypeSchema, + }), + t.partial({ + actions: t.array(action), + anomaly_threshold: t.number, + created_by: t.string, + false_positives: t.array(t.string), + filters: t.array(t.unknown), + from: t.string, + id: t.string, + index: t.array(t.string), + language: t.string, + machine_learning_job_id: t.string, + max_signals: t.number, + query: t.string, + references: t.array(t.string), + rule_id: t.string, + saved_id: t.string, + tags: t.array(t.string), + threat: t.array(t.unknown), + throttle: t.union([t.string, t.null]), + to: t.string, + updated_by: t.string, + note: t.string, + }), +]); + +export const NewRulesSchema = t.array(NewRuleSchema); +export type NewRule = t.TypeOf<typeof NewRuleSchema>; + +export interface AddRulesProps { + rule: NewRule; + signal: AbortSignal; +} + +const MetaRule = t.intersection([ + t.type({ + from: t.string, + }), + t.partial({ + throttle: t.string, + kibana_siem_app_url: t.string, + }), +]); + +export const RuleSchema = t.intersection([ + t.type({ + created_at: t.string, + created_by: t.string, + description: t.string, + enabled: t.boolean, + false_positives: t.array(t.string), + from: t.string, + id: t.string, + interval: t.string, + immutable: t.boolean, + name: t.string, + max_signals: t.number, + references: t.array(t.string), + risk_score: t.number, + rule_id: t.string, + severity: t.string, + tags: t.array(t.string), + type: RuleTypeSchema, + to: t.string, + threat: t.array(t.unknown), + updated_at: t.string, + updated_by: t.string, + actions: t.array(action), + throttle: t.union([t.string, t.null]), + }), + t.partial({ + anomaly_threshold: t.number, + filters: t.array(t.unknown), + index: t.array(t.string), + language: t.string, + last_failure_at: t.string, + last_failure_message: t.string, + meta: MetaRule, + machine_learning_job_id: t.string, + output_index: t.string, + query: t.string, + saved_id: t.string, + status: t.string, + status_date: t.string, + timeline_id: t.string, + timeline_title: t.string, + note: t.string, + version: t.number, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf<typeof RuleSchema>; +export type Rules = t.TypeOf<typeof RulesSchema>; + +export interface RuleError { + id?: string; + rule_id?: string; + error: { status_code: number; message: string }; +} + +export type BulkRuleResponse = Array<Rule | RuleError>; + +export interface RuleResponseBuckets { + rules: Rule[]; + errors: RuleError[]; +} + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; + showCustomRules?: boolean; + showElasticRules?: boolean; + tags?: string[]; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface FetchRuleProps { + id: string; + signal: AbortSignal; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; +} + +export interface DeleteRulesProps { + ids: string[]; +} + +export interface DuplicateRulesProps { + rules: Rule[]; +} + +export interface BasicFetchProps { + signal: AbortSignal; +} + +export interface ImportDataProps { + fileToImport: File; + overwrite?: boolean; + signal: AbortSignal; +} + +export interface ImportRulesResponseError { + rule_id: string; + error: { + status_code: number; + message: string; + }; +} + +export interface ImportDataResponse { + success: boolean; + success_count: number; + errors: ImportRulesResponseError[]; +} + +export interface ExportDocumentsProps { + ids: string[]; + filename?: string; + excludeExportDetails?: boolean; + signal: AbortSignal; +} + +export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { + alert_id: string; + status_date: string; + status: RuleStatusType | null; + last_failure_at: string | null; + last_success_at: string | null; + last_failure_message: string | null; + last_success_message: string | null; + last_look_back_date: string | null | undefined; + gap: string | null | undefined; + bulk_create_time_durations: string[] | null | undefined; + search_after_time_durations: string[] | null | undefined; +} + +export type RuleStatusResponse = Record<string, RuleStatus>; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx diff --git a/x-pack/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 0000000000000..1397e4a8696be --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_INDEX_URL, + DETECTION_ENGINE_PRIVILEGES_URL, +} from '../../../../common/constants'; +import { KibanaServices } from '../../../lib/kibana'; +import { + BasicSignals, + Privilege, + QuerySignals, + SignalSearchResponse, + SignalsIndex, + UpdateSignalStatusProps, +} from './types'; + +/** + * Fetch Signals by providing a query + * + * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchQuerySignals = async <Hit, Aggregations>({ + query, + signal, +}: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => + KibanaServices.get().http.fetch<SignalSearchResponse<Hit, Aggregations>>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + signal, + } + ); + +/** + * Update signal status by query + * + * @param query of signals to update + * @param status to update to('open' / 'closed') + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const updateSignalStatus = async ({ + query, + status, + signal, +}: UpdateSignalStatusProps): Promise<unknown> => + KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ status, ...query }), + signal, + }); + +/** + * Fetch Signal Index + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => + KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + +/** + * Get User Privileges + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => + KibanaServices.get().http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { + method: 'GET', + signal, + }); + +/** + * Create Signal Index if needed it + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => + KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/index.test.tsx b/x-pack/plugins/siem/public/containers/errors/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/index.test.tsx rename to x-pack/plugins/siem/public/containers/errors/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/index.tsx b/x-pack/plugins/siem/public/containers/errors/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/index.tsx rename to x-pack/plugins/siem/public/containers/errors/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/translations.ts b/x-pack/plugins/siem/public/containers/errors/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/translations.ts rename to x-pack/plugins/siem/public/containers/errors/translations.ts diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts new file mode 100644 index 0000000000000..9cae503d30940 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { QueryTemplateProps } from '../../query_template'; +import { useUiSetting$ } from '../../../lib/kibana'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; +import { useApolloClient } from '../../../utils/apollo_context'; + +export interface LastEventTimeArgs { + id: string; + errorMessage: string; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: LastEventTimeArgs) => React.ReactNode; + indexKey: LastEventIndexKey; +} + +export function useLastEventTimeQuery<TCache = object>( + indexKey: LastEventIndexKey, + details: LastTimeDetails, + sourceId: string +) { + const [loading, updateLoading] = useState(false); + const [lastSeen, updateLastSeen] = useState<number | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + const [currentIndexKey, updateCurrentIndexKey] = useState<LastEventIndexKey | null>(null); + const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const apolloClient = useApolloClient(); + async function fetchLastEventTime(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query<GetLastEventTimeQuery.Query, GetLastEventTimeQuery.Variables>({ + query: LastEventTimeGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + indexKey, + details, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); + updateErrorMessage(null); + updateCurrentIndexKey(currentIndexKey); + }, + error => { + updateLoading(false); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchLastEventTime(signal); + return () => abortCtrl.abort(); + }, [apolloClient, indexKey, details.hostName, details.ip]); + + return { lastSeen, loading, errorMessage }; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts rename to x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts new file mode 100644 index 0000000000000..43f55dfcf2777 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; + +interface MockLastEventTimeQuery { + request: { + query: GetLastEventTimeQuery.Query; + variables: GetLastEventTimeQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + LastEventTime: { + lastSeen: string | null; + errorMessage: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} + +const getTimeTwelveMinutesAgo = () => { + const d = new Date(); + const ts = d.getTime(); + const twelveMinutes = ts - 12 * 60 * 1000; + return new Date(twelveMinutes).toISOString(); +}; + +export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ + { + request: { + query: LastEventTimeGqlQuery, + variables: { + sourceId: 'default', + indexKey: LastEventIndexKey.hosts, + details: {}, + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + LastEventTime: { + lastSeen: getTimeTwelveMinutesAgo(), + errorMessage: null, + }, + }, + }, + }, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/plugins/siem/public/containers/global_time/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx rename to x-pack/plugins/siem/public/containers/global_time/index.tsx diff --git a/x-pack/plugins/siem/public/containers/helpers.test.ts b/x-pack/plugins/siem/public/containers/helpers.test.ts new file mode 100644 index 0000000000000..5d378d79acc7a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESQuery } from '../../common/typed_json'; + +import { createFilter } from './helpers'; + +describe('Helpers', () => { + describe('#createFilter', () => { + test('if it is a string it returns untouched', () => { + const filter = createFilter('even invalid strings return the same'); + expect(filter).toBe('even invalid strings return the same'); + }); + + test('if it is an ESQuery object it will be returned as a string', () => { + const query: ESQuery = { term: { 'host.id': 'host-value' } }; + const filter = createFilter(query); + expect(filter).toBe(JSON.stringify(query)); + }); + + test('if it is undefined, then undefined is returned', () => { + const filter = createFilter(undefined); + expect(filter).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/containers/helpers.ts b/x-pack/plugins/siem/public/containers/helpers.ts new file mode 100644 index 0000000000000..5f66e3f4b88d4 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FetchPolicy } from 'apollo-client'; +import { isString } from 'lodash/fp'; + +import { ESQuery } from '../../common/typed_json'; + +export const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts new file mode 100644 index 0000000000000..a460fa8999b57 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ApolloClient from 'apollo-client'; +import { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { QueryTemplateProps } from '../../query_template'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +export interface FirstLastSeenHostArgs { + id: string; + errorMessage: string; + firstSeen: Date; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: FirstLastSeenHostArgs) => React.ReactNode; + hostName: string; +} + +export function useFirstLastSeenHostQuery<TCache = object>( + hostName: string, + sourceId: string, + apolloClient: ApolloClient<TCache> +) { + const [loading, updateLoading] = useState(false); + const [firstSeen, updateFirstSeen] = useState<Date | null>(null); + const [lastSeen, updateLastSeen] = useState<Date | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + + async function fetchFirstLastSeenHost(signal: AbortSignal) { + updateLoading(true); + return apolloClient + .query<GetHostFirstLastSeenQuery.Query, GetHostFirstLastSeenQuery.Variables>({ + query: HostFirstLastSeenGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + hostName, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); + updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); + updateErrorMessage(null); + }, + error => { + updateLoading(false); + updateFirstSeen(null); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchFirstLastSeenHost(signal); + return () => abortCtrl.abort(); + }, []); + + return { firstSeen, lastSeen, loading, errorMessage }; +} diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts new file mode 100644 index 0000000000000..f59df84dacc1b --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +interface MockedProvidedQuery { + request: { + query: GetHostFirstLastSeenQuery.Query; + variables: GetHostFirstLastSeenQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + HostFirstLastSeen: { + firstSeen: string | null; + lastSeen: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} +export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ + { + request: { + query: HostFirstLastSeenGqlQuery, + variables: { + sourceId: 'default', + hostName: 'kibana-siem', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + HostFirstLastSeen: { + firstSeen: '2019-04-08T16:09:40.692Z', + lastSeen: '2019-04-08T18:35:45.064Z', + }, + }, + }, + }, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/index.tsx b/x-pack/plugins/siem/public/containers/hosts/index.tsx new file mode 100644 index 0000000000000..733c2224d840a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/index.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + Direction, + GetHostsTableQuery, + HostsEdges, + HostsFields, + PageInfoPaginated, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; + +import { HostsTableQuery } from './hosts_table.gql_query'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; + +const ID = 'hostsQuery'; + +export interface HostsArgs { + endDate: number; + hosts: HostsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: HostsArgs) => React.ReactNode; + type: hostsModel.HostsType; + startDate: number; + endDate: number; +} + +export interface HostsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sortField: HostsFields; + direction: Direction; +} + +type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; + +class HostsComponentQuery extends QueryTemplatePaginated< + HostsProps, + GetHostsTableQuery.Query, + GetHostsTableQuery.Variables +> { + private memoizedHosts: ( + variables: string, + data: GetHostsTableQuery.Source | undefined + ) => HostsEdges[]; + + constructor(props: HostsProps) { + super(props); + this.memoizedHosts = memoizeOne(this.getHosts); + } + + public render() { + const { + activePage, + id = ID, + isInspected, + children, + direction, + filterQuery, + endDate, + kibana, + limit, + startDate, + skip, + sourceId, + sortField, + } = this.props; + const defaultIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); + + const variables: GetHostsTableQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction, + field: sortField, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex, + inspect: isInspected, + }; + return ( + <Query<GetHostsTableQuery.Query, GetHostsTableQuery.Variables> + query={HostsTableQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={variables} + skip={skip} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Hosts: { + ...fetchMoreResult.source.Hosts, + edges: [...fetchMoreResult.source.Hosts.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + endDate, + hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), + id, + inspect: getOr(null, 'source.Hosts.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Hosts.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + startDate, + totalCount: getOr(-1, 'source.Hosts.totalCount', data), + }); + }} + </Query> + ); + } + + private getHosts = ( + variables: string, + source: GetHostsTableQuery.Source | undefined + ): HostsEdges[] => getOr([], 'Hosts.edges', source); +} + +const makeMapStateToProps = () => { + const getHostsSelector = hostsSelectors.hostsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHostsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(HostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx new file mode 100644 index 0000000000000..5057e872b5313 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, inputsSelectors, State } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplate, QueryTemplateProps } from '../../query_template'; +import { withKibana, WithKibanaProps } from '../../../lib/kibana'; + +import { HostOverviewQuery } from './host_overview.gql_query'; +import { GetHostOverviewQuery, HostItem } from '../../../graphql/types'; + +const ID = 'hostOverviewQuery'; + +export interface HostOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + hostOverview: HostItem; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + endDate: number; +} + +export interface HostOverviewReduxProps { + isInspected: boolean; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: HostOverviewArgs) => React.ReactNode; + hostName: string; + startDate: number; + endDate: number; +} + +type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; + +class HostOverviewByNameComponentQuery extends QueryTemplate< + HostsOverViewProps, + GetHostOverviewQuery.Query, + GetHostOverviewQuery.Variables +> { + public render() { + const { + id = ID, + isInspected, + children, + hostName, + kibana, + skip, + sourceId, + startDate, + endDate, + } = this.props; + return ( + <Query<GetHostOverviewQuery.Query, GetHostOverviewQuery.Variables> + query={HostOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const hostOverview = getOr([], 'source.HostOverview', data); + return children({ + id, + inspect: getOr(null, 'source.HostOverview.inspect', data), + refetch, + loading, + hostOverview, + startDate, + endDate, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostOverviewByNameQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(HostOverviewByNameComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/ip_overview/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/plugins/siem/public/containers/ip_overview/index.tsx new file mode 100644 index 0000000000000..ade94c430c6ef --- /dev/null +++ b/x-pack/plugins/siem/public/containers/ip_overview/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; +import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { ipOverviewQuery } from './index.gql_query'; + +const ID = 'ipOverviewQuery'; + +export interface IpOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + ipOverviewData: IpOverviewData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface IpOverviewProps extends QueryTemplateProps { + children: (args: IpOverviewArgs) => React.ReactNode; + type: networkModel.NetworkType; + ip: string; +} + +const IpOverviewComponentQuery = React.memo<IpOverviewProps & PropsFromRedux>( + ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( + <Query<GetIpOverviewQuery.Query, GetIpOverviewQuery.Variables> + query={ipOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + filterQuery: createFilter(filterQuery), + ip, + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const init: IpOverviewData = { host: {} }; + const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); + return children({ + id, + inspect: getOr(null, 'source.IpOverview.inspect', data), + ipOverviewData, + loading, + refetch, + }); + }} + </Query> + ) +); + +IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx b/x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx rename to x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx diff --git a/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx new file mode 100644 index 0000000000000..de9d54b1a185c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiHostDetailsQuery } from './index.gql_query'; + +const ID = 'kpiHostDetailsQuery'; + +export interface KpiHostDetailsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHostDetails: KpiHostDetailsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface QueryKpiHostDetailsProps extends QueryTemplateProps { + children: (args: KpiHostDetailsArgs) => React.ReactNode; +} + +const KpiHostDetailsComponentQuery = React.memo<QueryKpiHostDetailsProps & PropsFromRedux>( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + <Query<GetKpiHostDetailsQuery.Query, GetKpiHostDetailsQuery.Variables> + query={kpiHostDetailsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHostDetails.inspect', data), + kpiHostDetails, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx new file mode 100644 index 0000000000000..5be2423e8a162 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiHostsQuery } from './index.gql_query'; + +const ID = 'kpiHostsQuery'; + +export interface KpiHostsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHosts: KpiHostsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiHostsProps extends QueryTemplateProps { + children: (args: KpiHostsArgs) => React.ReactNode; +} + +const KpiHostsComponentQuery = React.memo<KpiHostsProps & PropsFromRedux>( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + <Query<GetKpiHostsQuery.Query, GetKpiHostsQuery.Variables> + query={kpiHostsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHosts = getOr({}, `source.KpiHosts`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHosts.inspect', data), + kpiHosts, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.gql_query.ts b/x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_network/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/plugins/siem/public/containers/kpi_network/index.tsx new file mode 100644 index 0000000000000..338cdc39b178c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_network/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiNetworkQuery } from './index.gql_query'; + +const ID = 'kpiNetworkQuery'; + +export interface KpiNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiNetwork: KpiNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiNetworkProps extends QueryTemplateProps { + children: (args: KpiNetworkArgs) => React.ReactNode; +} + +const KpiNetworkComponentQuery = React.memo<KpiNetworkProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( + <Query<GetKpiNetworkQuery.Query, GetKpiNetworkQuery.Variables> + query={kpiNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiNetwork = getOr({}, `source.KpiNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiNetwork.inspect', data), + kpiNetwork, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx new file mode 100644 index 0000000000000..6120538a01e78 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { QuerySuggestion, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { useKibana } from '../../lib/kibana'; + +type RendererResult = React.ReactElement<JSX.Element> | null; +type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result; + +interface KueryAutocompletionLifecycleProps { + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface KueryAutocompletionCurrentRequest { + expression: string; + cursorPosition: number; +} + +export const KueryAutocompletion = React.memo<KueryAutocompletionLifecycleProps>( + ({ children, indexPattern }) => { + const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>( + null + ); + const [suggestions, setSuggestions] = useState<QuerySuggestion[]>([]); + const kibana = useKibana(); + const loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number + ) => { + const language = 'kuery'; + + if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { + return; + } + + const futureRequest = { + expression, + cursorPosition, + }; + setCurrentRequest({ + expression, + cursorPosition, + }); + setSuggestions([]); + + if ( + futureRequest && + futureRequest.expression !== (currentRequest && currentRequest.expression) && + futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) + ) { + const newSuggestions = + (await kibana.services.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + })) || []; + + setCurrentRequest(null); + setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); + } + }; + + return children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions, + suggestions, + }); + } +); + +KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts b/x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx rename to x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts new file mode 100644 index 0000000000000..18bb611191bbc --- /dev/null +++ b/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { useEffect, useMemo, useState, useRef } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { useUiSetting$ } from '../../lib/kibana'; +import { createFilter } from '../helpers'; +import { useApolloClient } from '../../utils/apollo_context'; +import { inputsModel } from '../../store'; +import { MatrixHistogramGqlQuery } from './index.gql_query'; +import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; + +export const useQuery = <Hit, Aggs, TCache = object>({ + endDate, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, +}: MatrixHistogramQueryProps) => { + const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo<string[]>(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + const [, dispatchToaster] = useStateToaster(); + const refetch = useRef<inputsModel.Refetch>(); + const [loading, setLoading] = useState<boolean>(false); + const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); + const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); + const [totalCount, setTotalCount] = useState<number>(-1); + const apolloClient = useApolloClient(); + + useEffect(() => { + const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; + let isSubscribed = true; + const abortCtrl = new AbortController(); + const abortSignal = abortCtrl.signal; + + async function fetchData() { + if (!apolloClient) return null; + setLoading(true); + return apolloClient + .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ + query: MatrixHistogramGqlQuery, + fetchPolicy: 'network-only', + variables: matrixHistogramVariables, + context: { + fetchOptions: { + abortSignal, + }, + }, + }) + .then( + result => { + if (isSubscribed) { + const source = result?.data?.source?.MatrixHistogram ?? {}; + setData(source?.matrixHistogramData ?? []); + setTotalCount(source?.totalCount ?? -1); + setInspect(source?.inspect ?? null); + setLoading(false); + } + }, + error => { + if (isSubscribed) { + setData(null); + setTotalCount(-1); + setInspect(null); + setLoading(false); + errorToToaster({ title: errorMessage, error, dispatchToaster }); + } + } + ); + } + refetch.current = fetchData; + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + defaultIndex, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, + endDate, + data, + ]); + + return { data, loading, inspect, totalCount, refetch: refetch.current }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/plugins/siem/public/containers/network_dns/index.tsx new file mode 100644 index 0000000000000..04c8783c30a0f --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_dns/index.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DocumentNode } from 'graphql'; +import { ScaleType } from '@elastic/charts'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetNetworkDnsQuery, + NetworkDnsEdges, + NetworkDnsSortField, + PageInfoPaginated, + MatrixOverOrdinalHistogramData, +} from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkDnsQuery } from './index.gql_query'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; +import { MatrixHistogram } from '../../components/matrix_histogram'; +import { MatrixHistogramOption, GetSubTitle } from '../../components/matrix_histogram/types'; +import { UpdateDateRange } from '../../components/charts/common'; +import { SetQuery } from '../../pages/hosts/navigation/types'; + +const ID = 'networkDnsQuery'; +export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; +export interface NetworkDnsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkDns: NetworkDnsEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + stackByField?: string; + totalCount: number; + histogram: MatrixOverOrdinalHistogramData[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkDnsArgs) => React.ReactNode; + type: networkModel.NetworkType; +} + +interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { + dataKey: string | string[]; + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + isDnsHistogram?: boolean; + query: DocumentNode; + scaleType: ScaleType; + setQuery: SetQuery; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string; + type: networkModel.NetworkType; + updateDateRange: UpdateDateRange; + yTickFormatter?: (value: number) => string; +} + +export interface NetworkDnsComponentReduxProps { + activePage: number; + sort: NetworkDnsSortField; + isInspected: boolean; + isPtrIncluded: boolean; + limit: number; +} + +type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; + +export class NetworkDnsComponentQuery extends QueryTemplatePaginated< + NetworkDnsProps, + GetNetworkDnsQuery.Query, + GetNetworkDnsQuery.Variables +> { + public render() { + const { + activePage, + children, + sort, + endDate, + filterQuery, + id = ID, + isInspected, + isPtrIncluded, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetNetworkDnsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + isPtrIncluded, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + + return ( + <Query<GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkDnsQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkDns = getOr([], `source.NetworkDns.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkDns: { + ...fetchMoreResult.source.NetworkDns, + edges: [...fetchMoreResult.source.NetworkDns.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkDns.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkDns, + pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), + histogram: getOr(null, 'source.NetworkDns.histogram', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +export const NetworkDnsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkDnsComponentQuery); + +export const NetworkDnsHistogramQuery = compose<React.ComponentClass<DnsHistogramOwnProps>>( + connect(makeMapHistogramStateToProps), + withKibana +)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_http/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_http/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_http/index.tsx b/x-pack/plugins/siem/public/containers/network_http/index.tsx new file mode 100644 index 0000000000000..bf4e64f63d559 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_http/index.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetNetworkHttpQuery, + NetworkHttpEdges, + NetworkHttpSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkHttpQuery } from './index.gql_query'; + +const ID = 'networkHttpQuery'; + +export interface NetworkHttpArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkHttp: NetworkHttpEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkHttpArgs) => React.ReactNode; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkHttpComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkHttpSortField; +} + +type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; + +class NetworkHttpComponentQuery extends QueryTemplatePaginated< + NetworkHttpProps, + GetNetworkHttpQuery.Query, + GetNetworkHttpQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + sort, + startDate, + } = this.props; + const variables: GetNetworkHttpQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkHttpQuery.Query, GetNetworkHttpQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkHttpQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkHttp = getOr([], `source.NetworkHttp.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkHttp: { + ...fetchMoreResult.source.NetworkHttp, + edges: [...fetchMoreResult.source.NetworkHttp.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkHttp.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkHttp, + pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getHttpSelector = networkSelectors.httpSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHttpSelector(state, type), + isInspected, + }; + }; +}; + +export const NetworkHttpQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkHttpComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx new file mode 100644 index 0000000000000..bd1e1a002bbcd --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopCountriesQuery, + NetworkTopCountriesEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkTopCountriesQuery } from './index.gql_query'; + +const ID = 'networkTopCountriesQuery'; + +export interface NetworkTopCountriesArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopCountries: NetworkTopCountriesEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopCountriesArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopCountriesComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; + +class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< + NetworkTopCountriesProps, + GetNetworkTopCountriesQuery.Query, + GetNetworkTopCountriesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopCountriesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkTopCountriesQuery.Query, GetNetworkTopCountriesQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopCountriesQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopCountries: { + ...fetchMoreResult.source.NetworkTopCountries, + edges: [...fetchMoreResult.source.NetworkTopCountries.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopCountries, + pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopCountriesSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopCountriesQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx new file mode 100644 index 0000000000000..f0f1f8257f29f --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopNFlowQuery, + NetworkTopNFlowEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkTopNFlowQuery } from './index.gql_query'; + +const ID = 'networkTopNFlowQuery'; + +export interface NetworkTopNFlowArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopNFlow: NetworkTopNFlowEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopNFlowArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopNFlowComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; + +class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< + NetworkTopNFlowProps, + GetNetworkTopNFlowQuery.Query, + GetNetworkTopNFlowQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopNFlowQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkTopNFlowQuery.Query, GetNetworkTopNFlowQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopNFlowQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopNFlow: { + ...fetchMoreResult.source.NetworkTopNFlow, + edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopNFlow, + pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTopNFlowSelector = networkSelectors.topNFlowSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopNFlowSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopNFlowQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts b/x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx new file mode 100644 index 0000000000000..2dd9ccf24d802 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; +import { inputsModel, inputsSelectors } from '../../../store/inputs'; +import { State } from '../../../store'; +import { createFilter, getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { overviewHostQuery } from './index.gql_query'; + +export const ID = 'overviewHostQuery'; + +export interface OverviewHostArgs { + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + overviewHost: OverviewHostData; + refetch: inputsModel.Refetch; +} + +export interface OverviewHostProps extends QueryTemplateProps { + children: (args: OverviewHostArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +const OverviewHostComponentQuery = React.memo<OverviewHostProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { + return ( + <Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables> + query={overviewHostQuery} + fetchPolicy={getDefaultFetchPolicy()} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewHost = getOr({}, `source.OverviewHost`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewHost.inspect', data), + overviewHost, + loading, + refetch, + }); + }} + </Query> + ); + } +); + +OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts b/x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx new file mode 100644 index 0000000000000..d0acd41c224a5 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; +import { State } from '../../../store'; +import { inputsModel, inputsSelectors } from '../../../store/inputs'; +import { createFilter, getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { overviewNetworkQuery } from './index.gql_query'; + +export const ID = 'overviewNetworkQuery'; + +export interface OverviewNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + overviewNetwork: OverviewNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OverviewNetworkProps extends QueryTemplateProps { + children: (args: OverviewNetworkArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +export const OverviewNetworkComponentQuery = React.memo<OverviewNetworkProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( + <Query<GetOverviewNetworkQuery.Query, GetOverviewNetworkQuery.Variables> + query={overviewNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewNetwork.inspect', data), + overviewNetwork, + loading, + refetch, + }); + }} + </Query> + ) +); + +OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx b/x-pack/plugins/siem/public/containers/query_template.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/query_template.tsx rename to x-pack/plugins/siem/public/containers/query_template.tsx index c33f5fd89a79b..dfb452c24b86e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx +++ b/x-pack/plugins/siem/public/containers/query_template.tsx @@ -8,7 +8,7 @@ import { ApolloQueryResult } from 'apollo-client'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; +import { ESQuery } from '../../common/typed_json'; export interface QueryTemplateProps { id?: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/plugins/siem/public/containers/query_template_paginated.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx rename to x-pack/plugins/siem/public/containers/query_template_paginated.tsx index 45041a6447611..db618f216d83e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/plugins/siem/public/containers/query_template_paginated.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import deepEqual from 'fast-deep-equal'; -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; +import { ESQuery } from '../../common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.gql_query.ts b/x-pack/plugins/siem/public/containers/source/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/source/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/source/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.test.tsx b/x-pack/plugins/siem/public/containers/source/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/source/index.test.tsx rename to x-pack/plugins/siem/public/containers/source/index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/source/index.tsx b/x-pack/plugins/siem/public/containers/source/index.tsx new file mode 100644 index 0000000000000..e9359fdb19587 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/source/index.tsx @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isUndefined } from 'lodash'; +import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; +import { Query } from 'react-apollo'; +import React, { useEffect, useMemo, useState } from 'react'; +import memoizeOne from 'memoize-one'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +import { IndexField, SourceQuery } from '../../graphql/types'; + +import { sourceQuery } from './index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; + +export { sourceQuery }; + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly<Record<string, Partial<BrowserField>>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; +} + +export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>; + +export const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> => + Object.values(browserFields).reduce<Array<Partial<BrowserField>>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +export const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial<BrowserField> } => + keyBy('name', getAllBrowserFields(browserFields)); + +interface WithSourceArgs { + indicesExist: boolean; + browserFields: BrowserFields; + indexPattern: IIndexPattern; +} + +interface WithSourceProps { + children: (args: WithSourceArgs) => React.ReactNode; + indexToAdd?: string[] | null; + sourceId: string; +} + +export const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): IIndexPattern => + fields && fields.length > 0 + ? { + fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), + title, + } + : { fields: [], title } +); + +export const getBrowserFields = memoizeOne( + (title: string, fields: IndexField[]): BrowserFields => + fields && fields.length > 0 + ? fields.reduce<BrowserFields>( + (accumulator: BrowserFields, field: IndexField) => + set([field.category, 'fields', field.name], field, accumulator), + {} + ) + : {} +); + +export const WithSource = React.memo<WithSourceProps>(({ children, indexToAdd, sourceId }) => { + const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo<string[]>(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + return ( + <Query<SourceQuery.Query, SourceQuery.Variables> + query={sourceQuery} + fetchPolicy="cache-first" + notifyOnNetworkStatusChange + variables={{ + sourceId, + defaultIndex, + }} + > + {({ data }) => + children({ + indicesExist: get('source.status.indicesExist', data), + browserFields: getBrowserFields( + defaultIndex.join(), + get('source.status.indexFields', data) + ), + indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), + }) + } + </Query> + ); +}); + +WithSource.displayName = 'WithSource'; + +export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => + indicesExist || isUndefined(indicesExist); + +export const useWithSource = (sourceId: string, indices: string[]) => { + const [loading, updateLoading] = useState(false); + const [indicesExist, setIndicesExist] = useState<boolean | undefined | null>(undefined); + const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null); + const [indexPattern, setIndexPattern] = useState<IIndexPattern | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + + const apolloClient = useApolloClient(); + async function fetchSource(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query<SourceQuery.Query, SourceQuery.Variables>({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateErrorMessage(null); + setIndicesExist(get('data.source.status.indicesExist', result)); + setBrowserFields( + getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) + ); + setIndexPattern( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + }, + error => { + updateLoading(false); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchSource(signal); + return () => abortCtrl.abort(); + }, [apolloClient, sourceId, indices]); + + return { indicesExist, browserFields, indexPattern, loading, errorMessage }; +}; diff --git a/x-pack/plugins/siem/public/containers/source/mock.ts b/x-pack/plugins/siem/public/containers/source/mock.ts new file mode 100644 index 0000000000000..092aad9e7400c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/source/mock.ts @@ -0,0 +1,699 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; + +import { BrowserFields } from '.'; +import { sourceQuery } from './index.gql_query'; + +export const mocksSource = [ + { + request: { + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + configuration: {}, + status: { + indicesExist: true, + winlogbeatIndices: [ + 'winlogbeat-7.0.0-2019.02.17', + 'winlogbeat-7.0.0-2019.02.18', + 'winlogbeat-7.0.0-2019.02.19', + 'winlogbeat-7.0.0-2019.02.20', + 'winlogbeat-7.0.0-2019.02.21', + 'winlogbeat-7.0.0-2019.02.21-000001', + 'winlogbeat-7.0.0-2019.02.22', + 'winlogbeat-8.0.0-2019.02.19-000001', + ], + auditbeatIndices: [ + 'auditbeat-7.0.0-2019.02.17', + 'auditbeat-7.0.0-2019.02.18', + 'auditbeat-7.0.0-2019.02.19', + 'auditbeat-7.0.0-2019.02.20', + 'auditbeat-7.0.0-2019.02.21', + 'auditbeat-7.0.0-2019.02.21-000001', + 'auditbeat-7.0.0-2019.02.22', + 'auditbeat-8.0.0-2019.02.19-000001', + ], + filebeatIndices: [ + 'filebeat-7.0.0-iot-2019.06', + 'filebeat-7.0.0-iot-2019.07', + 'filebeat-7.0.0-iot-2019.08', + 'filebeat-7.0.0-iot-2019.09', + 'filebeat-7.0.0-iot-2019.10', + 'filebeat-8.0.0-2019.02.19-000001', + ], + indexFields: [ + { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'source', + description: + 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + }, + ], + }, + }, + }, + }, + }, +]; + +export const mockIndexFields = [ + { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, + { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, +]; + +export const mockBrowserFields: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + 'agent.name': { + aggregatable: true, + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + 'auditd.data.a1': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + }, + 'auditd.data.a2': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + }, + }, + }, + base: { + fields: { + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + }, + }, + client: { + fields: { + 'client.address': { + aggregatable: true, + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + }, + 'client.bytes': { + aggregatable: true, + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + 'cloud.availability_zone': { + aggregatable: true, + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + 'container.image.name': { + aggregatable: true, + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + }, + 'container.image.tag': { + aggregatable: true, + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + }, + }, + }, + destination: { + fields: { + 'destination.address': { + aggregatable: true, + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + }, + 'destination.bytes': { + aggregatable: true, + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + }, + 'destination.domain': { + aggregatable: true, + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts similarity index 94% rename from x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts index e380e46e77070..7d30b6c22a110 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts @@ -55,6 +55,9 @@ export const allTimelinesQuery = gql` noteIds pinnedEventIds title + timelineType + templateTimelineId + templateTimelineVersion created createdBy updated diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx new file mode 100644 index 0000000000000..62c8d21a2e944 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, noop } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { OpenTimelineResult } from '../../../components/open_timeline/types'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { + GetAllTimeline, + PageInfoTimeline, + SortTimeline, + TimelineResult, +} from '../../../graphql/types'; +import { inputsModel, inputsActions } from '../../../store/inputs'; +import { useApolloClient } from '../../../utils/apollo_context'; + +import { allTimelinesQuery } from './index.gql_query'; +import * as i18n from '../../../pages/timelines/translations'; + +export interface AllTimelinesArgs { + fetchAllTimeline: ({ onlyUserFavorite, pageInfo, search, sort }: AllTimelinesVariables) => void; + timelines: OpenTimelineResult[]; + loading: boolean; + totalCount: number; + refetch: () => void; +} + +export interface AllTimelinesVariables { + onlyUserFavorite: boolean; + pageInfo: PageInfoTimeline; + search: string; + sort: SortTimeline; + timelines: OpenTimelineResult[]; + totalCount: number; +} + +export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; + +export const getAllTimeline = memoizeOne( + (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => + timelines.map(timeline => ({ + created: timeline.created, + description: timeline.description, + eventIdToNoteIds: + timeline.eventIdToNoteIds != null + ? timeline.eventIdToNoteIds.reduce((acc, note) => { + if (note.eventId != null) { + const notes = getOr([], note.eventId, acc); + return { ...acc, [note.eventId]: [...notes, note.noteId] }; + } + return acc; + }, {}) + : null, + favorite: timeline.favorite, + noteIds: timeline.noteIds, + notes: + timeline.notes != null + ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) + : null, + pinnedEventIds: + timeline.pinnedEventIds != null + ? timeline.pinnedEventIds.reduce( + (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), + {} + ) + : null, + savedObjectId: timeline.savedObjectId, + title: timeline.title, + updated: timeline.updated, + updatedBy: timeline.updatedBy, + })) +); + +export const useGetAllTimeline = (): AllTimelinesArgs => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + const refetch = useRef<inputsModel.Refetch>(); + const [, dispatchToaster] = useStateToaster(); + const [allTimelines, setAllTimelines] = useState<AllTimelinesArgs>({ + fetchAllTimeline: noop, + loading: false, + refetch: refetch.current ?? noop, + totalCount: 0, + timelines: [], + }); + + const fetchAllTimeline = useCallback( + async ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelines, + totalCount, + }: AllTimelinesVariables) => { + let didCancel = false; + const abortCtrl = new AbortController(); + + const fetchData = async () => { + try { + if (apolloClient != null) { + setAllTimelines({ + ...allTimelines, + timelines: timelines ?? allTimelines.timelines, + totalCount: totalCount ?? allTimelines.totalCount, + loading: true, + }); + const variables: GetAllTimeline.Variables = { + onlyUserFavorite, + pageInfo, + search, + sort, + }; + const response = await apolloClient.query< + GetAllTimeline.Query, + GetAllTimeline.Variables + >({ + query: allTimelinesQuery, + fetchPolicy: 'network-only', + variables, + context: { + fetchOptions: { + abortSignal: abortCtrl.signal, + }, + }, + }); + if (!didCancel) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: ALL_TIMELINE_QUERY_ID, + loading: false, + refetch: refetch.current ?? noop, + inspect: null, + }) + ); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: refetch.current ?? noop, + totalCount: getOr(0, 'getAllTimeline.totalCount', response.data), + timelines: getAllTimeline( + JSON.stringify(variables), + getOr([], 'getAllTimeline.timeline', response.data) + ), + }); + } + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.ERROR_FETCHING_TIMELINES_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: noop, + totalCount: 0, + timelines: [], + }); + } + } + }; + refetch.current = fetchData; + fetchData(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [apolloClient, allTimelines] + ); + + useEffect(() => { + return () => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID })); + }; + }, [dispatch]); + + return { + ...allTimelines, + fetchAllTimeline, + refetch: refetch.current ?? noop, + }; +}; diff --git a/x-pack/plugins/siem/public/containers/timeline/api.ts b/x-pack/plugins/siem/public/containers/timeline/api.ts new file mode 100644 index 0000000000000..023e2e6af9f88 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/api.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { throwErrors } from '../../../../case/common/api'; +import { + SavedTimeline, + TimelineResponse, + TimelineResponseType, +} from '../../../common/types/timeline'; +import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; + +import { KibanaServices } from '../../lib/kibana'; +import { ExportSelectedData } from '../../components/generic_downloader'; + +import { createToasterPlainError } from '../case/utils'; +import { ImportDataProps, ImportDataResponse } from '../detection_engine/rules'; + +interface RequestPostTimeline { + timeline: SavedTimeline; + signal?: AbortSignal; +} + +interface RequestPatchTimeline<T = string> extends RequestPostTimeline { + timelineId: T; + version: T; +} + +type RequestPersistTimeline = RequestPostTimeline & Partial<RequestPatchTimeline<null | string>>; + +const decodeTimelineResponse = (respTimeline?: TimelineResponse) => + pipe( + TimelineResponseType.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + +const postTimeline = async ({ timeline }: RequestPostTimeline): Promise<TimelineResponse> => { + const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_URL, { + method: 'POST', + body: JSON.stringify({ timeline }), + }); + + return decodeTimelineResponse(response); +}; + +const patchTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPatchTimeline): Promise<TimelineResponse> => { + const response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, { + method: 'PATCH', + body: JSON.stringify({ timeline, timelineId, version }), + }); + + return decodeTimelineResponse(response); +}; + +export const persistTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPersistTimeline): Promise<TimelineResponse> => { + if (timelineId == null) { + return postTimeline({ timeline }); + } + return patchTimeline({ + timelineId, + timeline, + version: version ?? '', + }); +}; + +export const importTimelines = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise<ImportDataResponse> => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + }); +}; + +export const exportSelectedTimeline: ExportSelectedData = async ({ + excludeExportDetails = false, + filename = `timelines_export.ndjson`, + ids = [], + signal, +}): Promise<Blob> => { + const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; + const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); + + return response.body!; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/plugins/siem/public/containers/timeline/details/index.tsx new file mode 100644 index 0000000000000..cf1b8954307e7 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/details/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; + +import { timelineDetailsQuery } from './index.gql_query'; + +export interface EventsArgs { + detailsData: DetailItem[] | null; + loading: boolean; +} + +export interface TimelineDetailsProps { + children?: (args: EventsArgs) => React.ReactElement; + indexName: string; + eventId: string; + executeQuery: boolean; + sourceId: string; +} + +const getDetailsEvent = memoizeOne( + (variables: string, detail: DetailItem[]): DetailItem[] => detail +); + +const TimelineDetailsQueryComponent: React.FC<TimelineDetailsProps> = ({ + children, + indexName, + eventId, + executeQuery, + sourceId, +}) => { + const variables: GetTimelineDetailsQuery.Variables = { + sourceId, + indexName, + eventId, + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + }; + return executeQuery ? ( + <Query<GetTimelineDetailsQuery.Query, GetTimelineDetailsQuery.Variables> + query={timelineDetailsQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, refetch }) => + children!({ + loading, + detailsData: getDetailsEvent( + JSON.stringify(variables), + getOr([], 'source.TimelineDetails.data', data) + ), + }) + } + </Query> + ) : ( + children!({ loading: false, detailsData: null }) + ); +}; + +export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/index.tsx b/x-pack/plugins/siem/public/containers/timeline/index.tsx new file mode 100644 index 0000000000000..6e09e124696b6 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/index.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, uniqBy } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { compose, Dispatch } from 'redux'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import { + GetTimelineQuery, + PageInfo, + SortField, + TimelineEdges, + TimelineItem, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter } from '../helpers'; +import { QueryTemplate, QueryTemplateProps } from '../query_template'; +import { EventType } from '../../store/timeline/model'; +import { timelineQuery } from './index.gql_query'; +import { timelineActions } from '../../store/timeline'; +import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals'; + +export interface TimelineArgs { + events: TimelineItem[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + loadMore: (cursor: string, tieBreaker: string) => void; + pageInfo: PageInfo; + refetch: inputsModel.Refetch; + totalCount: number; + getUpdatedAt: () => number; +} + +export interface CustomReduxProps { + clearSignalsState: ({ id }: { id?: string }) => void; +} + +export interface OwnProps extends QueryTemplateProps { + children?: (args: TimelineArgs) => React.ReactNode; + eventType?: EventType; + id: string; + indexPattern?: IIndexPattern; + indexToAdd?: string[]; + limit: number; + sortField: SortField; + fields: string[]; +} + +type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; + +class TimelineQueryComponent extends QueryTemplate< + TimelineQueryProps, + GetTimelineQuery.Query, + GetTimelineQuery.Variables +> { + private updatedDate: number = Date.now(); + private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; + + constructor(props: TimelineQueryProps) { + super(props); + this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); + } + + public render() { + const { + children, + clearSignalsState, + eventType = 'raw', + id, + indexPattern, + indexToAdd = [], + isInspected, + kibana, + limit, + fields, + filterQuery, + sourceId, + sortField, + } = this.props; + const defaultKibanaIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = + indexPattern == null || (indexPattern != null && indexPattern.title === '') + ? [ + ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), + ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), + ] + : indexPattern?.title.split(',') ?? []; + const variables: GetTimelineQuery.Variables = { + fieldRequested: fields, + filterQuery: createFilter(filterQuery), + sourceId, + pagination: { limit, cursor: null, tiebreaker: null }, + sortField, + defaultIndex, + inspect: isInspected, + }; + + return ( + <Query<GetTimelineQuery.Query, GetTimelineQuery.Variables> + query={timelineQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, fetchMore, refetch }) => { + this.setRefetch(refetch); + this.setExecuteBeforeRefetch(clearSignalsState); + this.setExecuteBeforeFetchMore(clearSignalsState); + + const timelineEdges = getOr([], 'source.Timeline.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ + variables: { + pagination: { + cursor: newCursor, + tiebreaker, + limit, + }, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Timeline: { + ...fetchMoreResult.source.Timeline, + edges: uniqBy('node._id', [ + ...prev.source.Timeline.edges, + ...fetchMoreResult.source.Timeline.edges, + ]), + }, + }, + }; + }, + })); + this.updatedDate = Date.now(); + return children!({ + id, + inspect: getOr(null, 'source.Timeline.inspect', data), + refetch: this.wrappedRefetch, + loading, + totalCount: getOr(0, 'source.Timeline.totalCount', data), + pageInfo: getOr({}, 'source.Timeline.pageInfo', data), + events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), + loadMore: this.wrappedLoadMore, + getUpdatedAt: this.getUpdatedAt, + }); + }} + </Query> + ); + } + + private getUpdatedAt = () => this.updatedDate; + + private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => + timelineEdges.map((e: TimelineEdges) => e.node); +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSignalsState: ({ id }: { id?: string }) => { + if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { + dispatch(timelineActions.clearEventsLoading({ id })); + dispatch(timelineActions.clearEventsDeleted({ id })); + } + }, +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const TimelineQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(TimelineQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts b/x-pack/plugins/siem/public/containers/tls/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/tls/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/tls/index.tsx b/x-pack/plugins/siem/public/containers/tls/index.tsx new file mode 100644 index 0000000000000..3738355c8846e --- /dev/null +++ b/x-pack/plugins/siem/public/containers/tls/index.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + PageInfoPaginated, + TlsEdges, + TlsSortField, + GetTlsQuery, + FlowTargetSourceDest, +} from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { tlsQuery } from './index.gql_query'; + +const ID = 'tlsQuery'; + +export interface TlsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + tls: TlsEdges[]; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: TlsArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip: string; + type: networkModel.NetworkType; +} + +export interface TlsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: TlsSortField; +} + +type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; + +class TlsComponentQuery extends QueryTemplatePaginated< + TlsProps, + GetTlsQuery.Query, + GetTlsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetTlsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate ? startDate : 0, + to: endDate ? endDate : Date.now(), + }, + }; + return ( + <Query<GetTlsQuery.Query, GetTlsQuery.Variables> + query={tlsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const tls = getOr([], 'source.Tls.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Tls: { + ...fetchMoreResult.source.Tls, + edges: [...fetchMoreResult.source.Tls.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Tls.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Tls.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + tls, + totalCount: getOr(-1, 'source.Tls.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTlsSelector = networkSelectors.tlsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTlsSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const TlsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(TlsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts b/x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx new file mode 100644 index 0000000000000..0a2ce67d9be80 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetUncommonProcessesQuery, + PageInfoPaginated, + UncommonProcessesEdges, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { uncommonProcessesQuery } from './index.gql_query'; + +const ID = 'uncommonProcessesQuery'; + +export interface UncommonProcessesArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + uncommonProcesses: UncommonProcessesEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UncommonProcessesArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UncommonProcessesComponentQuery extends QueryTemplatePaginated< + UncommonProcessesProps, + GetUncommonProcessesQuery.Query, + GetUncommonProcessesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetUncommonProcessesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + pagination: generateTablePaginationOptions(activePage, limit), + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables> + query={uncommonProcessesQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + UncommonProcesses: { + ...fetchMoreResult.source.UncommonProcesses, + edges: [...fetchMoreResult.source.UncommonProcesses.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.UncommonProcesses.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), + uncommonProcesses, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUncommonProcessesSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const UncommonProcessesQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(UncommonProcessesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/users/index.gql_query.ts b/x-pack/plugins/siem/public/containers/users/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/users/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/users/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/users/index.tsx b/x-pack/plugins/siem/public/containers/users/index.tsx new file mode 100644 index 0000000000000..5f71449c52460 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/users/index.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { usersQuery } from './index.gql_query'; + +const ID = 'usersQuery'; + +export interface UsersArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + users: UsersEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UsersArgs) => React.ReactNode; + flowTarget: FlowTarget; + ip: string; + type: networkModel.NetworkType; +} + +type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UsersComponentQuery extends QueryTemplatePaginated< + UsersProps, + GetUsersQuery.Query, + GetUsersQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetUsersQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetUsersQuery.Query, GetUsersQuery.Variables> + query={usersQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const users = getOr([], `source.Users.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Users: { + ...fetchMoreResult.source.Users, + edges: [...fetchMoreResult.source.Users.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Users.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Users.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Users.totalCount', data), + users, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getUsersSelector = networkSelectors.usersSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUsersSelector(state), + isInspected, + }; + }; + + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const UsersQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(UsersComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json similarity index 99% rename from x-pack/legacy/plugins/siem/public/graphql/introspection.json rename to x-pack/plugins/siem/public/graphql/introspection.json index 2a9dd8f2aacfe..4026a043c7778 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -9728,6 +9728,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "templateTimelineId", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timelineType", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updated", "description": "", @@ -10323,6 +10347,39 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TimelineType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "default", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "template", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", @@ -10863,6 +10920,24 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "templateTimelineId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "defaultValue": null + }, + { + "name": "timelineType", + "description": "", + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "defaultValue": null + }, { "name": "dateRange", "description": "", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts new file mode 100644 index 0000000000000..8c39d5e58b99e --- /dev/null +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -0,0 +1,5966 @@ +/* tslint:disable */ +/* eslint-disable */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type Maybe<T> = T | null; + +export interface PageInfoNote { + pageIndex: number; + + pageSize: number; +} + +export interface SortNote { + sortField: SortFieldNote; + + sortOrder: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: number; + /** The beginning of the timerange */ + from: number; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe<string>; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe<string>; +} + +export interface SortField { + sortFieldId: string; + + direction: Direction; +} + +export interface LastTimeDetails { + hostName?: Maybe<string>; + + ip?: Maybe<string>; +} + +export interface HostsSortField { + field: HostsFields; + + direction: Direction; +} + +export interface UsersSortField { + field: UsersFields; + + direction: Direction; +} + +export interface NetworkTopTablesSortField { + field: NetworkTopTablesFields; + + direction: Direction; +} + +export interface NetworkDnsSortField { + field: NetworkDnsFields; + + direction: Direction; +} + +export interface NetworkHttpSortField { + direction: Direction; +} + +export interface TlsSortField { + field: TlsFields; + + direction: Direction; +} + +export interface PageInfoTimeline { + pageIndex: number; + + pageSize: number; +} + +export interface SortTimeline { + sortField: SortFieldTimeline; + + sortOrder: Direction; +} + +export interface NoteInput { + eventId?: Maybe<string>; + + note?: Maybe<string>; + + timelineId?: Maybe<string>; +} + +export interface TimelineInput { + columns?: Maybe<ColumnHeaderInput[]>; + + dataProviders?: Maybe<DataProviderInput[]>; + + description?: Maybe<string>; + + eventType?: Maybe<string>; + + filters?: Maybe<FilterTimelineInput[]>; + + kqlMode?: Maybe<string>; + + kqlQuery?: Maybe<SerializedFilterQueryInput>; + + title?: Maybe<string>; + + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + + dateRange?: Maybe<DateRangePickerInput>; + + savedQueryId?: Maybe<string>; + + sort?: Maybe<SortTimelineInput>; +} + +export interface ColumnHeaderInput { + aggregatable?: Maybe<boolean>; + + category?: Maybe<string>; + + columnHeaderType?: Maybe<string>; + + description?: Maybe<string>; + + example?: Maybe<string>; + + indexes?: Maybe<string[]>; + + id?: Maybe<string>; + + name?: Maybe<string>; + + placeholder?: Maybe<string>; + + searchable?: Maybe<boolean>; + + type?: Maybe<string>; +} + +export interface DataProviderInput { + id?: Maybe<string>; + + name?: Maybe<string>; + + enabled?: Maybe<boolean>; + + excluded?: Maybe<boolean>; + + kqlQuery?: Maybe<string>; + + queryMatch?: Maybe<QueryMatchInput>; + + and?: Maybe<DataProviderInput[]>; +} + +export interface QueryMatchInput { + field?: Maybe<string>; + + displayField?: Maybe<string>; + + value?: Maybe<string>; + + displayValue?: Maybe<string>; + + operator?: Maybe<string>; +} + +export interface FilterTimelineInput { + exists?: Maybe<string>; + + meta?: Maybe<FilterMetaTimelineInput>; + + match_all?: Maybe<string>; + + missing?: Maybe<string>; + + query?: Maybe<string>; + + range?: Maybe<string>; + + script?: Maybe<string>; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe<string>; + + controlledBy?: Maybe<string>; + + disabled?: Maybe<boolean>; + + field?: Maybe<string>; + + formattedValue?: Maybe<string>; + + index?: Maybe<string>; + + key?: Maybe<string>; + + negate?: Maybe<boolean>; + + params?: Maybe<string>; + + type?: Maybe<string>; + + value?: Maybe<string>; +} + +export interface SerializedFilterQueryInput { + filterQuery?: Maybe<SerializedKueryQueryInput>; +} + +export interface SerializedKueryQueryInput { + kuery?: Maybe<KueryFilterQueryInput>; + + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryInput { + kind?: Maybe<string>; + + expression?: Maybe<string>; +} + +export interface DateRangePickerInput { + start?: Maybe<number>; + + end?: Maybe<number>; +} + +export interface SortTimelineInput { + columnId?: Maybe<string>; + + sortDirection?: Maybe<string>; +} + +export interface FavoriteTimelineInput { + fullName?: Maybe<string>; + + userName?: Maybe<string>; + + favoriteDate?: Maybe<number>; +} + +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export enum LastEventIndexKey { + hostDetails = 'hostDetails', + hosts = 'hosts', + ipDetails = 'ipDetails', + network = 'network', +} + +export enum HostsFields { + hostName = 'hostName', + lastSeen = 'lastSeen', +} + +export enum UsersFields { + name = 'name', + count = 'count', +} + +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + +export enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} + +export enum NetworkTopTablesFields { + bytes_in = 'bytes_in', + bytes_out = 'bytes_out', + flows = 'flows', + destination_ips = 'destination_ips', + source_ips = 'source_ips', +} + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export enum TlsFields { + _id = '_id', +} + +export enum TimelineType { + default = 'default', + template = 'template', +} + +export enum SortFieldTimeline { + title = 'title', + description = 'description', + updated = 'updated', + created = 'created', +} + +export enum NetworkDirectionEcs { + inbound = 'inbound', + outbound = 'outbound', + internal = 'internal', + external = 'external', + incoming = 'incoming', + outgoing = 'outgoing', + listening = 'listening', + unknown = 'unknown', +} + +export enum NetworkHttpFields { + domains = 'domains', + lastHost = 'lastHost', + lastSourceIp = 'lastSourceIp', + methods = 'methods', + path = 'path', + requestCount = 'requestCount', + statuses = 'statuses', +} + +export enum FlowDirection { + uniDirectional = 'uniDirectional', + biDirectional = 'biDirectional', +} + +export type ToStringArray = string[]; + +export type Date = string; + +export type ToNumberArray = number[]; + +export type ToDateArray = string[]; + +export type ToBooleanArray = boolean[]; + +export type ToAny = any; + +export type EsValue = any; + +// ==================================================== +// Scalars +// ==================================================== + +// ==================================================== +// Types +// ==================================================== + +export interface Query { + getNote: NoteResult; + + getNotesByTimelineId: NoteResult[]; + + getNotesByEventId: NoteResult[]; + + getAllNotes: ResponseNotes; + + getAllPinnedEventsByTimelineId: PinnedEvent[]; + /** Get a security data source by id */ + source: Source; + /** Get a list of all security data sources */ + allSources: Source[]; + + getOneTimeline: TimelineResult; + + getAllTimeline: ResponseTimelines; +} + +export interface NoteResult { + eventId?: Maybe<string>; + + note?: Maybe<string>; + + timelineId?: Maybe<string>; + + noteId: string; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} + +export interface ResponseNotes { + notes: NoteResult[]; + + totalCount?: Maybe<number>; +} + +export interface PinnedEvent { + code?: Maybe<number>; + + message?: Maybe<string>; + + pinnedEventId: string; + + eventId?: Maybe<string>; + + timelineId?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} + +export interface Source { + /** The id of the source */ + id: string; + /** The raw configuration of the source */ + configuration: SourceConfiguration; + /** The status of the source */ + status: SourceStatus; + /** Gets Authentication success and failures based on a timerange */ + Authentications: AuthenticationsData; + + Timeline: TimelineData; + + TimelineDetails: TimelineDetailsData; + + LastEventTime: LastEventTimeData; + /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ + Hosts: HostsData; + + HostOverview: HostItem; + + HostFirstLastSeen: FirstLastSeenHost; + + IpOverview?: Maybe<IpOverviewData>; + + Users: UsersData; + + KpiNetwork?: Maybe<KpiNetworkData>; + + KpiHosts: KpiHostsData; + + KpiHostDetails: KpiHostDetailsData; + + MatrixHistogram: MatrixHistogramOverTimeData; + + NetworkTopCountries: NetworkTopCountriesData; + + NetworkTopNFlow: NetworkTopNFlowData; + + NetworkDns: NetworkDnsData; + + NetworkDnsHistogram: NetworkDsOverTimeData; + + NetworkHttp: NetworkHttpData; + + OverviewNetwork?: Maybe<OverviewNetworkData>; + + OverviewHost?: Maybe<OverviewHostData>; + + Tls: TlsData; + /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ + UncommonProcesses: UncommonProcessesData; + /** Just a simple example to get the app name */ + whoAmI?: Maybe<SayMyName>; +} + +/** A set of configuration options for a security data source */ +export interface SourceConfiguration { + /** The field mapping to use for this source */ + fields: SourceFields; +} + +/** A mapping of semantic fields to their document counterparts */ +export interface SourceFields { + /** The field to identify a container by */ + container: string; + /** The fields to identify a host by */ + host: string; + /** The fields that may contain the log event message. The first field found win. */ + message: string[]; + /** The field to identify a pod by */ + pod: string; + /** The field to use as a tiebreaker for log events that have identical timestamps */ + tiebreaker: string; + /** The field to use as a timestamp for metrics and logs */ + timestamp: string; +} + +/** The status of an infrastructure data source */ +export interface SourceStatus { + /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ + indicesExist: boolean; + /** The list of fields defined in the index mappings */ + indexFields: IndexField[]; +} + +/** A descriptor of a field in an index */ +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe<string>; + /** whether the field's belong to an alias index */ + indexes: (Maybe<string>)[]; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe<string>; + + format?: Maybe<string>; +} + +export interface AuthenticationsData { + edges: AuthenticationsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface AuthenticationsEdges { + node: AuthenticationItem; + + cursor: CursorType; +} + +export interface AuthenticationItem { + _id: string; + + failures: number; + + successes: number; + + user: UserEcsFields; + + lastSuccess?: Maybe<LastSourceHost>; + + lastFailure?: Maybe<LastSourceHost>; +} + +export interface UserEcsFields { + domain?: Maybe<string[]>; + + id?: Maybe<string[]>; + + name?: Maybe<string[]>; + + full_name?: Maybe<string[]>; + + email?: Maybe<string[]>; + + hash?: Maybe<string[]>; + + group?: Maybe<string[]>; +} + +export interface LastSourceHost { + timestamp?: Maybe<string>; + + source?: Maybe<SourceEcsFields>; + + host?: Maybe<HostEcsFields>; +} + +export interface SourceEcsFields { + bytes?: Maybe<number[]>; + + ip?: Maybe<string[]>; + + port?: Maybe<number[]>; + + domain?: Maybe<string[]>; + + geo?: Maybe<GeoEcsFields>; + + packets?: Maybe<number[]>; +} + +export interface GeoEcsFields { + city_name?: Maybe<string[]>; + + continent_name?: Maybe<string[]>; + + country_iso_code?: Maybe<string[]>; + + country_name?: Maybe<string[]>; + + location?: Maybe<Location>; + + region_iso_code?: Maybe<string[]>; + + region_name?: Maybe<string[]>; +} + +export interface Location { + lon?: Maybe<number[]>; + + lat?: Maybe<number[]>; +} + +export interface HostEcsFields { + architecture?: Maybe<string[]>; + + id?: Maybe<string[]>; + + ip?: Maybe<string[]>; + + mac?: Maybe<string[]>; + + name?: Maybe<string[]>; + + os?: Maybe<OsEcsFields>; + + type?: Maybe<string[]>; +} + +export interface OsEcsFields { + platform?: Maybe<string[]>; + + name?: Maybe<string[]>; + + full?: Maybe<string[]>; + + family?: Maybe<string[]>; + + version?: Maybe<string[]>; + + kernel?: Maybe<string[]>; +} + +export interface CursorType { + value?: Maybe<string>; + + tiebreaker?: Maybe<string>; +} + +export interface PageInfoPaginated { + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface TimelineData { + edges: TimelineEdges[]; + + totalCount: number; + + pageInfo: PageInfo; + + inspect?: Maybe<Inspect>; +} + +export interface TimelineEdges { + node: TimelineItem; + + cursor: CursorType; +} + +export interface TimelineItem { + _id: string; + + _index?: Maybe<string>; + + data: TimelineNonEcsData[]; + + ecs: Ecs; +} + +export interface TimelineNonEcsData { + field: string; + + value?: Maybe<string[]>; +} + +export interface Ecs { + _id: string; + + _index?: Maybe<string>; + + auditd?: Maybe<AuditdEcsFields>; + + destination?: Maybe<DestinationEcsFields>; + + dns?: Maybe<DnsEcsFields>; + + endgame?: Maybe<EndgameEcsFields>; + + event?: Maybe<EventEcsFields>; + + geo?: Maybe<GeoEcsFields>; + + host?: Maybe<HostEcsFields>; + + network?: Maybe<NetworkEcsField>; + + rule?: Maybe<RuleEcsField>; + + signal?: Maybe<SignalField>; + + source?: Maybe<SourceEcsFields>; + + suricata?: Maybe<SuricataEcsFields>; + + tls?: Maybe<TlsEcsFields>; + + zeek?: Maybe<ZeekEcsFields>; + + http?: Maybe<HttpEcsFields>; + + url?: Maybe<UrlEcsFields>; + + timestamp?: Maybe<string>; + + message?: Maybe<string[]>; + + user?: Maybe<UserEcsFields>; + + winlog?: Maybe<WinlogEcsFields>; + + process?: Maybe<ProcessEcsFields>; + + file?: Maybe<FileFields>; + + system?: Maybe<SystemEcsField>; +} + +export interface AuditdEcsFields { + result?: Maybe<string[]>; + + session?: Maybe<string[]>; + + data?: Maybe<AuditdData>; + + summary?: Maybe<Summary>; + + sequence?: Maybe<string[]>; +} + +export interface AuditdData { + acct?: Maybe<string[]>; + + terminal?: Maybe<string[]>; + + op?: Maybe<string[]>; +} + +export interface Summary { + actor?: Maybe<PrimarySecondary>; + + object?: Maybe<PrimarySecondary>; + + how?: Maybe<string[]>; + + message_type?: Maybe<string[]>; + + sequence?: Maybe<string[]>; +} + +export interface PrimarySecondary { + primary?: Maybe<string[]>; + + secondary?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface DestinationEcsFields { + bytes?: Maybe<number[]>; + + ip?: Maybe<string[]>; + + port?: Maybe<number[]>; + + domain?: Maybe<string[]>; + + geo?: Maybe<GeoEcsFields>; + + packets?: Maybe<number[]>; +} + +export interface DnsEcsFields { + question?: Maybe<DnsQuestionData>; + + resolved_ip?: Maybe<string[]>; + + response_code?: Maybe<string[]>; +} + +export interface DnsQuestionData { + name?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface EndgameEcsFields { + exit_code?: Maybe<number[]>; + + file_name?: Maybe<string[]>; + + file_path?: Maybe<string[]>; + + logon_type?: Maybe<number[]>; + + parent_process_name?: Maybe<string[]>; + + pid?: Maybe<number[]>; + + process_name?: Maybe<string[]>; + + subject_domain_name?: Maybe<string[]>; + + subject_logon_id?: Maybe<string[]>; + + subject_user_name?: Maybe<string[]>; + + target_domain_name?: Maybe<string[]>; + + target_logon_id?: Maybe<string[]>; + + target_user_name?: Maybe<string[]>; +} + +export interface EventEcsFields { + action?: Maybe<string[]>; + + category?: Maybe<string[]>; + + code?: Maybe<string[]>; + + created?: Maybe<string[]>; + + dataset?: Maybe<string[]>; + + duration?: Maybe<number[]>; + + end?: Maybe<string[]>; + + hash?: Maybe<string[]>; + + id?: Maybe<string[]>; + + kind?: Maybe<string[]>; + + module?: Maybe<string[]>; + + original?: Maybe<string[]>; + + outcome?: Maybe<string[]>; + + risk_score?: Maybe<number[]>; + + risk_score_norm?: Maybe<number[]>; + + severity?: Maybe<number[]>; + + start?: Maybe<string[]>; + + timezone?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface NetworkEcsField { + bytes?: Maybe<number[]>; + + community_id?: Maybe<string[]>; + + direction?: Maybe<string[]>; + + packets?: Maybe<number[]>; + + protocol?: Maybe<string[]>; + + transport?: Maybe<string[]>; +} + +export interface RuleEcsField { + reference?: Maybe<string[]>; +} + +export interface SignalField { + rule?: Maybe<RuleField>; + + original_time?: Maybe<string[]>; +} + +export interface RuleField { + id?: Maybe<string[]>; + + rule_id?: Maybe<string[]>; + + false_positives: string[]; + + saved_id?: Maybe<string[]>; + + timeline_id?: Maybe<string[]>; + + timeline_title?: Maybe<string[]>; + + max_signals?: Maybe<number[]>; + + risk_score?: Maybe<string[]>; + + output_index?: Maybe<string[]>; + + description?: Maybe<string[]>; + + from?: Maybe<string[]>; + + immutable?: Maybe<boolean[]>; + + index?: Maybe<string[]>; + + interval?: Maybe<string[]>; + + language?: Maybe<string[]>; + + query?: Maybe<string[]>; + + references?: Maybe<string[]>; + + severity?: Maybe<string[]>; + + tags?: Maybe<string[]>; + + threat?: Maybe<ToAny>; + + type?: Maybe<string[]>; + + size?: Maybe<string[]>; + + to?: Maybe<string[]>; + + enabled?: Maybe<boolean[]>; + + filters?: Maybe<ToAny>; + + created_at?: Maybe<string[]>; + + updated_at?: Maybe<string[]>; + + created_by?: Maybe<string[]>; + + updated_by?: Maybe<string[]>; + + version?: Maybe<string[]>; + + note?: Maybe<string[]>; +} + +export interface SuricataEcsFields { + eve?: Maybe<SuricataEveData>; +} + +export interface SuricataEveData { + alert?: Maybe<SuricataAlertData>; + + flow_id?: Maybe<number[]>; + + proto?: Maybe<string[]>; +} + +export interface SuricataAlertData { + signature?: Maybe<string[]>; + + signature_id?: Maybe<number[]>; +} + +export interface TlsEcsFields { + client_certificate?: Maybe<TlsClientCertificateData>; + + fingerprints?: Maybe<TlsFingerprintsData>; + + server_certificate?: Maybe<TlsServerCertificateData>; +} + +export interface TlsClientCertificateData { + fingerprint?: Maybe<FingerprintData>; +} + +export interface FingerprintData { + sha1?: Maybe<string[]>; +} + +export interface TlsFingerprintsData { + ja3?: Maybe<TlsJa3Data>; +} + +export interface TlsJa3Data { + hash?: Maybe<string[]>; +} + +export interface TlsServerCertificateData { + fingerprint?: Maybe<FingerprintData>; +} + +export interface ZeekEcsFields { + session_id?: Maybe<string[]>; + + connection?: Maybe<ZeekConnectionData>; + + notice?: Maybe<ZeekNoticeData>; + + dns?: Maybe<ZeekDnsData>; + + http?: Maybe<ZeekHttpData>; + + files?: Maybe<ZeekFileData>; + + ssl?: Maybe<ZeekSslData>; +} + +export interface ZeekConnectionData { + local_resp?: Maybe<boolean[]>; + + local_orig?: Maybe<boolean[]>; + + missed_bytes?: Maybe<number[]>; + + state?: Maybe<string[]>; + + history?: Maybe<string[]>; +} + +export interface ZeekNoticeData { + suppress_for?: Maybe<number[]>; + + msg?: Maybe<string[]>; + + note?: Maybe<string[]>; + + sub?: Maybe<string[]>; + + dst?: Maybe<string[]>; + + dropped?: Maybe<boolean[]>; + + peer_descr?: Maybe<string[]>; +} + +export interface ZeekDnsData { + AA?: Maybe<boolean[]>; + + qclass_name?: Maybe<string[]>; + + RD?: Maybe<boolean[]>; + + qtype_name?: Maybe<string[]>; + + rejected?: Maybe<boolean[]>; + + qtype?: Maybe<string[]>; + + query?: Maybe<string[]>; + + trans_id?: Maybe<number[]>; + + qclass?: Maybe<string[]>; + + RA?: Maybe<boolean[]>; + + TC?: Maybe<boolean[]>; +} + +export interface ZeekHttpData { + resp_mime_types?: Maybe<string[]>; + + trans_depth?: Maybe<string[]>; + + status_msg?: Maybe<string[]>; + + resp_fuids?: Maybe<string[]>; + + tags?: Maybe<string[]>; +} + +export interface ZeekFileData { + session_ids?: Maybe<string[]>; + + timedout?: Maybe<boolean[]>; + + local_orig?: Maybe<boolean[]>; + + tx_host?: Maybe<string[]>; + + source?: Maybe<string[]>; + + is_orig?: Maybe<boolean[]>; + + overflow_bytes?: Maybe<number[]>; + + sha1?: Maybe<string[]>; + + duration?: Maybe<number[]>; + + depth?: Maybe<number[]>; + + analyzers?: Maybe<string[]>; + + mime_type?: Maybe<string[]>; + + rx_host?: Maybe<string[]>; + + total_bytes?: Maybe<number[]>; + + fuid?: Maybe<string[]>; + + seen_bytes?: Maybe<number[]>; + + missing_bytes?: Maybe<number[]>; + + md5?: Maybe<string[]>; +} + +export interface ZeekSslData { + cipher?: Maybe<string[]>; + + established?: Maybe<boolean[]>; + + resumed?: Maybe<boolean[]>; + + version?: Maybe<string[]>; +} + +export interface HttpEcsFields { + version?: Maybe<string[]>; + + request?: Maybe<HttpRequestData>; + + response?: Maybe<HttpResponseData>; +} + +export interface HttpRequestData { + method?: Maybe<string[]>; + + body?: Maybe<HttpBodyData>; + + referrer?: Maybe<string[]>; + + bytes?: Maybe<number[]>; +} + +export interface HttpBodyData { + content?: Maybe<string[]>; + + bytes?: Maybe<number[]>; +} + +export interface HttpResponseData { + status_code?: Maybe<number[]>; + + body?: Maybe<HttpBodyData>; + + bytes?: Maybe<number[]>; +} + +export interface UrlEcsFields { + domain?: Maybe<string[]>; + + original?: Maybe<string[]>; + + username?: Maybe<string[]>; + + password?: Maybe<string[]>; +} + +export interface WinlogEcsFields { + event_id?: Maybe<number[]>; +} + +export interface ProcessEcsFields { + hash?: Maybe<ProcessHashData>; + + pid?: Maybe<number[]>; + + name?: Maybe<string[]>; + + ppid?: Maybe<number[]>; + + args?: Maybe<string[]>; + + executable?: Maybe<string[]>; + + title?: Maybe<string[]>; + + thread?: Maybe<Thread>; + + working_directory?: Maybe<string[]>; +} + +export interface ProcessHashData { + md5?: Maybe<string[]>; + + sha1?: Maybe<string[]>; + + sha256?: Maybe<string[]>; +} + +export interface Thread { + id?: Maybe<number[]>; + + start?: Maybe<string[]>; +} + +export interface FileFields { + name?: Maybe<string[]>; + + path?: Maybe<string[]>; + + target_path?: Maybe<string[]>; + + extension?: Maybe<string[]>; + + type?: Maybe<string[]>; + + device?: Maybe<string[]>; + + inode?: Maybe<string[]>; + + uid?: Maybe<string[]>; + + owner?: Maybe<string[]>; + + gid?: Maybe<string[]>; + + group?: Maybe<string[]>; + + mode?: Maybe<string[]>; + + size?: Maybe<number[]>; + + mtime?: Maybe<string[]>; + + ctime?: Maybe<string[]>; +} + +export interface SystemEcsField { + audit?: Maybe<AuditEcsFields>; + + auth?: Maybe<AuthEcsFields>; +} + +export interface AuditEcsFields { + package?: Maybe<PackageEcsFields>; +} + +export interface PackageEcsFields { + arch?: Maybe<string[]>; + + entity_id?: Maybe<string[]>; + + name?: Maybe<string[]>; + + size?: Maybe<number[]>; + + summary?: Maybe<string[]>; + + version?: Maybe<string[]>; +} + +export interface AuthEcsFields { + ssh?: Maybe<SshEcsFields>; +} + +export interface SshEcsFields { + method?: Maybe<string[]>; + + signature?: Maybe<string[]>; +} + +export interface PageInfo { + endCursor?: Maybe<CursorType>; + + hasNextPage?: Maybe<boolean>; +} + +export interface TimelineDetailsData { + data?: Maybe<DetailItem[]>; + + inspect?: Maybe<Inspect>; +} + +export interface DetailItem { + field: string; + + values?: Maybe<string[]>; + + originalValue?: Maybe<EsValue>; +} + +export interface LastEventTimeData { + lastSeen?: Maybe<string>; + + inspect?: Maybe<Inspect>; +} + +export interface HostsData { + edges: HostsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface HostsEdges { + node: HostItem; + + cursor: CursorType; +} + +export interface HostItem { + _id?: Maybe<string>; + + lastSeen?: Maybe<string>; + + host?: Maybe<HostEcsFields>; + + cloud?: Maybe<CloudFields>; + + inspect?: Maybe<Inspect>; +} + +export interface CloudFields { + instance?: Maybe<CloudInstance>; + + machine?: Maybe<CloudMachine>; + + provider?: Maybe<(Maybe<string>)[]>; + + region?: Maybe<(Maybe<string>)[]>; +} + +export interface CloudInstance { + id?: Maybe<(Maybe<string>)[]>; +} + +export interface CloudMachine { + type?: Maybe<(Maybe<string>)[]>; +} + +export interface FirstLastSeenHost { + inspect?: Maybe<Inspect>; + + firstSeen?: Maybe<string>; + + lastSeen?: Maybe<string>; +} + +export interface IpOverviewData { + client?: Maybe<Overview>; + + destination?: Maybe<Overview>; + + host: HostEcsFields; + + server?: Maybe<Overview>; + + source?: Maybe<Overview>; + + inspect?: Maybe<Inspect>; +} + +export interface Overview { + firstSeen?: Maybe<string>; + + lastSeen?: Maybe<string>; + + autonomousSystem: AutonomousSystem; + + geo: GeoEcsFields; +} + +export interface AutonomousSystem { + number?: Maybe<number>; + + organization?: Maybe<AutonomousSystemOrganization>; +} + +export interface AutonomousSystemOrganization { + name?: Maybe<string>; +} + +export interface UsersData { + edges: UsersEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface UsersEdges { + node: UsersNode; + + cursor: CursorType; +} + +export interface UsersNode { + _id?: Maybe<string>; + + timestamp?: Maybe<string>; + + user?: Maybe<UsersItem>; +} + +export interface UsersItem { + name?: Maybe<string>; + + id?: Maybe<string[]>; + + groupId?: Maybe<string[]>; + + groupName?: Maybe<string[]>; + + count?: Maybe<number>; +} + +export interface KpiNetworkData { + networkEvents?: Maybe<number>; + + uniqueFlowId?: Maybe<number>; + + uniqueSourcePrivateIps?: Maybe<number>; + + uniqueSourcePrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; + + uniqueDestinationPrivateIps?: Maybe<number>; + + uniqueDestinationPrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; + + dnsQueries?: Maybe<number>; + + tlsHandshakes?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface KpiNetworkHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; +} + +export interface KpiHostsData { + hosts?: Maybe<number>; + + hostsHistogram?: Maybe<KpiHostHistogramData[]>; + + authSuccess?: Maybe<number>; + + authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; + + authFailure?: Maybe<number>; + + authFailureHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueSourceIps?: Maybe<number>; + + uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueDestinationIps?: Maybe<number>; + + uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + inspect?: Maybe<Inspect>; +} + +export interface KpiHostHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; +} + +export interface KpiHostDetailsData { + authSuccess?: Maybe<number>; + + authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; + + authFailure?: Maybe<number>; + + authFailureHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueSourceIps?: Maybe<number>; + + uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueDestinationIps?: Maybe<number>; + + uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + inspect?: Maybe<Inspect>; +} + +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + +export interface NetworkTopCountriesData { + edges: NetworkTopCountriesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe<string>; + + source?: Maybe<TopCountriesItemSource>; + + destination?: Maybe<TopCountriesItemDestination>; + + network?: Maybe<TopNetworkTablesEcsField>; +} + +export interface TopCountriesItemSource { + country?: Maybe<string>; + + destination_ips?: Maybe<number>; + + flows?: Maybe<number>; + + location?: Maybe<GeoItem>; + + source_ips?: Maybe<number>; +} + +export interface GeoItem { + geo?: Maybe<GeoEcsFields>; + + flowTarget?: Maybe<FlowTargetSourceDest>; +} + +export interface TopCountriesItemDestination { + country?: Maybe<string>; + + destination_ips?: Maybe<number>; + + flows?: Maybe<number>; + + location?: Maybe<GeoItem>; + + source_ips?: Maybe<number>; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe<number>; + + bytes_out?: Maybe<number>; +} + +export interface NetworkTopNFlowData { + edges: NetworkTopNFlowEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkTopNFlowEdges { + node: NetworkTopNFlowItem; + + cursor: CursorType; +} + +export interface NetworkTopNFlowItem { + _id?: Maybe<string>; + + source?: Maybe<TopNFlowItemSource>; + + destination?: Maybe<TopNFlowItemDestination>; + + network?: Maybe<TopNetworkTablesEcsField>; +} + +export interface TopNFlowItemSource { + autonomous_system?: Maybe<AutonomousSystemItem>; + + domain?: Maybe<string[]>; + + ip?: Maybe<string>; + + location?: Maybe<GeoItem>; + + flows?: Maybe<number>; + + destination_ips?: Maybe<number>; +} + +export interface AutonomousSystemItem { + name?: Maybe<string>; + + number?: Maybe<number>; +} + +export interface TopNFlowItemDestination { + autonomous_system?: Maybe<AutonomousSystemItem>; + + domain?: Maybe<string[]>; + + ip?: Maybe<string>; + + location?: Maybe<GeoItem>; + + flows?: Maybe<number>; + + source_ips?: Maybe<number>; +} + +export interface NetworkDnsData { + edges: NetworkDnsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; + + histogram?: Maybe<MatrixOverOrdinalHistogramData[]>; +} + +export interface NetworkDnsEdges { + node: NetworkDnsItem; + + cursor: CursorType; +} + +export interface NetworkDnsItem { + _id?: Maybe<string>; + + dnsBytesIn?: Maybe<number>; + + dnsBytesOut?: Maybe<number>; + + dnsName?: Maybe<string>; + + queryCount?: Maybe<number>; + + uniqueDomains?: Maybe<number>; +} + +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + +export interface NetworkDsOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface NetworkHttpData { + edges: NetworkHttpEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkHttpEdges { + node: NetworkHttpItem; + + cursor: CursorType; +} + +export interface NetworkHttpItem { + _id?: Maybe<string>; + + domains: string[]; + + lastHost?: Maybe<string>; + + lastSourceIp?: Maybe<string>; + + methods: string[]; + + path?: Maybe<string>; + + requestCount?: Maybe<number>; + + statuses: string[]; +} + +export interface OverviewNetworkData { + auditbeatSocket?: Maybe<number>; + + filebeatCisco?: Maybe<number>; + + filebeatNetflow?: Maybe<number>; + + filebeatPanw?: Maybe<number>; + + filebeatSuricata?: Maybe<number>; + + filebeatZeek?: Maybe<number>; + + packetbeatDNS?: Maybe<number>; + + packetbeatFlow?: Maybe<number>; + + packetbeatTLS?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface OverviewHostData { + auditbeatAuditd?: Maybe<number>; + + auditbeatFIM?: Maybe<number>; + + auditbeatLogin?: Maybe<number>; + + auditbeatPackage?: Maybe<number>; + + auditbeatProcess?: Maybe<number>; + + auditbeatUser?: Maybe<number>; + + endgameDns?: Maybe<number>; + + endgameFile?: Maybe<number>; + + endgameImageLoad?: Maybe<number>; + + endgameNetwork?: Maybe<number>; + + endgameProcess?: Maybe<number>; + + endgameRegistry?: Maybe<number>; + + endgameSecurity?: Maybe<number>; + + filebeatSystemModule?: Maybe<number>; + + winlogbeatSecurity?: Maybe<number>; + + winlogbeatMWSysmonOperational?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface TlsData { + edges: TlsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface TlsEdges { + node: TlsNode; + + cursor: CursorType; +} + +export interface TlsNode { + _id?: Maybe<string>; + + timestamp?: Maybe<string>; + + notAfter?: Maybe<string[]>; + + subjects?: Maybe<string[]>; + + ja3?: Maybe<string[]>; + + issuers?: Maybe<string[]>; +} + +export interface UncommonProcessesData { + edges: UncommonProcessesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface UncommonProcessesEdges { + node: UncommonProcessItem; + + cursor: CursorType; +} + +export interface UncommonProcessItem { + _id: string; + + instances: number; + + process: ProcessEcsFields; + + hosts: HostEcsFields[]; + + user?: Maybe<UserEcsFields>; +} + +export interface SayMyName { + /** The id of the source */ + appName: string; +} + +export interface TimelineResult { + columns?: Maybe<ColumnHeaderResult[]>; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + dataProviders?: Maybe<DataProviderResult[]>; + + dateRange?: Maybe<DateRangePickerResult>; + + description?: Maybe<string>; + + eventIdToNoteIds?: Maybe<NoteResult[]>; + + eventType?: Maybe<string>; + + favorite?: Maybe<FavoriteTimelineResult[]>; + + filters?: Maybe<FilterTimelineResult[]>; + + kqlMode?: Maybe<string>; + + kqlQuery?: Maybe<SerializedFilterQueryResult>; + + notes?: Maybe<NoteResult[]>; + + noteIds?: Maybe<string[]>; + + pinnedEventIds?: Maybe<string[]>; + + pinnedEventsSaveObject?: Maybe<PinnedEvent[]>; + + savedQueryId?: Maybe<string>; + + savedObjectId: string; + + sort?: Maybe<SortTimelineResult>; + + title?: Maybe<string>; + + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version: string; +} + +export interface ColumnHeaderResult { + aggregatable?: Maybe<boolean>; + + category?: Maybe<string>; + + columnHeaderType?: Maybe<string>; + + description?: Maybe<string>; + + example?: Maybe<string>; + + indexes?: Maybe<string[]>; + + id?: Maybe<string>; + + name?: Maybe<string>; + + placeholder?: Maybe<string>; + + searchable?: Maybe<boolean>; + + type?: Maybe<string>; +} + +export interface DataProviderResult { + id?: Maybe<string>; + + name?: Maybe<string>; + + enabled?: Maybe<boolean>; + + excluded?: Maybe<boolean>; + + kqlQuery?: Maybe<string>; + + queryMatch?: Maybe<QueryMatchResult>; + + and?: Maybe<DataProviderResult[]>; +} + +export interface QueryMatchResult { + field?: Maybe<string>; + + displayField?: Maybe<string>; + + value?: Maybe<string>; + + displayValue?: Maybe<string>; + + operator?: Maybe<string>; +} + +export interface DateRangePickerResult { + start?: Maybe<number>; + + end?: Maybe<number>; +} + +export interface FavoriteTimelineResult { + fullName?: Maybe<string>; + + userName?: Maybe<string>; + + favoriteDate?: Maybe<number>; +} + +export interface FilterTimelineResult { + exists?: Maybe<string>; + + meta?: Maybe<FilterMetaTimelineResult>; + + match_all?: Maybe<string>; + + missing?: Maybe<string>; + + query?: Maybe<string>; + + range?: Maybe<string>; + + script?: Maybe<string>; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe<string>; + + controlledBy?: Maybe<string>; + + disabled?: Maybe<boolean>; + + field?: Maybe<string>; + + formattedValue?: Maybe<string>; + + index?: Maybe<string>; + + key?: Maybe<string>; + + negate?: Maybe<boolean>; + + params?: Maybe<string>; + + type?: Maybe<string>; + + value?: Maybe<string>; +} + +export interface SerializedFilterQueryResult { + filterQuery?: Maybe<SerializedKueryQueryResult>; +} + +export interface SerializedKueryQueryResult { + kuery?: Maybe<KueryFilterQueryResult>; + + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryResult { + kind?: Maybe<string>; + + expression?: Maybe<string>; +} + +export interface SortTimelineResult { + columnId?: Maybe<string>; + + sortDirection?: Maybe<string>; +} + +export interface ResponseTimelines { + timeline: (Maybe<TimelineResult>)[]; + + totalCount?: Maybe<number>; +} + +export interface Mutation { + /** Persists a note */ + persistNote: ResponseNote; + + deleteNote?: Maybe<boolean>; + + deleteNoteByTimelineId?: Maybe<boolean>; + /** Persists a pinned event in a timeline */ + persistPinnedEventOnTimeline?: Maybe<PinnedEvent>; + /** Remove a pinned events in a timeline */ + deletePinnedEventOnTimeline: boolean; + /** Remove all pinned events in a timeline */ + deleteAllPinnedEventsOnTimeline: boolean; + /** Persists a timeline */ + persistTimeline: ResponseTimeline; + + persistFavorite: ResponseFavoriteTimeline; + + deleteTimeline: boolean; +} + +export interface ResponseNote { + code?: Maybe<number>; + + message?: Maybe<string>; + + note: NoteResult; +} + +export interface ResponseTimeline { + code?: Maybe<number>; + + message?: Maybe<string>; + + timeline: TimelineResult; +} + +export interface ResponseFavoriteTimeline { + code?: Maybe<number>; + + message?: Maybe<string>; + + savedObjectId: string; + + version: string; + + favorite?: Maybe<FavoriteTimelineResult[]>; +} + +export interface EcsEdges { + node: Ecs; + + cursor: CursorType; +} + +export interface EventsTimelineData { + edges: EcsEdges[]; + + totalCount: number; + + pageInfo: PageInfo; + + inspect?: Maybe<Inspect>; +} + +export interface OsFields { + platform?: Maybe<string>; + + name?: Maybe<string>; + + full?: Maybe<string>; + + family?: Maybe<string>; + + version?: Maybe<string>; + + kernel?: Maybe<string>; +} + +export interface HostFields { + architecture?: Maybe<string>; + + id?: Maybe<string>; + + ip?: Maybe<(Maybe<string>)[]>; + + mac?: Maybe<(Maybe<string>)[]>; + + name?: Maybe<string>; + + os?: Maybe<OsFields>; + + type?: Maybe<string>; +} + +// ==================================================== +// Arguments +// ==================================================== + +export interface GetNoteQueryArgs { + id: string; +} +export interface GetNotesByTimelineIdQueryArgs { + timelineId: string; +} +export interface GetNotesByEventIdQueryArgs { + eventId: string; +} +export interface GetAllNotesQueryArgs { + pageInfo?: Maybe<PageInfoNote>; + + search?: Maybe<string>; + + sort?: Maybe<SortNote>; +} +export interface GetAllPinnedEventsByTimelineIdQueryArgs { + timelineId: string; +} +export interface SourceQueryArgs { + /** The id of the source */ + id: string; +} +export interface GetOneTimelineQueryArgs { + id: string; +} +export interface GetAllTimelineQueryArgs { + pageInfo?: Maybe<PageInfoTimeline>; + + search?: Maybe<string>; + + sort?: Maybe<SortTimeline>; + + onlyUserFavorite?: Maybe<boolean>; +} +export interface AuthenticationsSourceArgs { + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TimelineSourceArgs { + pagination: PaginationInput; + + sortField: SortField; + + fieldRequested: string[]; + + timerange?: Maybe<TimerangeInput>; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TimelineDetailsSourceArgs { + eventId: string; + + indexName: string; + + defaultIndex: string[]; +} +export interface LastEventTimeSourceArgs { + id?: Maybe<string>; + + indexKey: LastEventIndexKey; + + details: LastTimeDetails; + + defaultIndex: string[]; +} +export interface HostsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + sort: HostsSortField; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface HostOverviewSourceArgs { + id?: Maybe<string>; + + hostName: string; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface HostFirstLastSeenSourceArgs { + id?: Maybe<string>; + + hostName: string; + + defaultIndex: string[]; +} +export interface IpOverviewSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip: string; + + defaultIndex: string[]; +} +export interface UsersSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: UsersSortField; + + flowTarget: FlowTarget; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface KpiNetworkSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface KpiHostsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface KpiHostDetailsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} +export interface NetworkTopCountriesSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkTopNFlowSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkDnsSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + isPtrIncluded: boolean; + + pagination: PaginationInputPaginated; + + sort: NetworkDnsSortField; + + stackByField?: Maybe<string>; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkDnsHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField?: Maybe<string>; +} +export interface NetworkHttpSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + pagination: PaginationInputPaginated; + + sort: NetworkHttpSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface OverviewNetworkSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface OverviewHostSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TlsSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: TlsSortField; + + flowTarget: FlowTargetSourceDest; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface UncommonProcessesSourceArgs { + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface IndicesExistSourceStatusArgs { + defaultIndex: string[]; +} +export interface IndexFieldsSourceStatusArgs { + defaultIndex: string[]; +} +export interface PersistNoteMutationArgs { + noteId?: Maybe<string>; + + version?: Maybe<string>; + + note: NoteInput; +} +export interface DeleteNoteMutationArgs { + id: string[]; +} +export interface DeleteNoteByTimelineIdMutationArgs { + timelineId: string; + + version?: Maybe<string>; +} +export interface PersistPinnedEventOnTimelineMutationArgs { + pinnedEventId?: Maybe<string>; + + eventId: string; + + timelineId?: Maybe<string>; +} +export interface DeletePinnedEventOnTimelineMutationArgs { + id: string[]; +} +export interface DeleteAllPinnedEventsOnTimelineMutationArgs { + timelineId: string; +} +export interface PersistTimelineMutationArgs { + id?: Maybe<string>; + + version?: Maybe<string>; + + timeline: TimelineInput; +} +export interface PersistFavoriteMutationArgs { + timelineId?: Maybe<string>; +} +export interface DeleteTimelineMutationArgs { + id: string[]; +} + +// ==================================================== +// Documents +// ==================================================== + +export namespace GetAuthenticationsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Authentications: Authentications; + }; + + export type Authentications = { + __typename?: 'AuthenticationsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'AuthenticationsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'AuthenticationItem'; + + _id: string; + + failures: number; + + successes: number; + + user: User; + + lastSuccess: Maybe<LastSuccess>; + + lastFailure: Maybe<LastFailure>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + name: Maybe<string[]>; + }; + + export type LastSuccess = { + __typename?: 'LastSourceHost'; + + timestamp: Maybe<string>; + + source: Maybe<_Source>; + + host: Maybe<Host>; + }; + + export type _Source = { + __typename?: 'SourceEcsFields'; + + ip: Maybe<string[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type LastFailure = { + __typename?: 'LastSourceHost'; + + timestamp: Maybe<string>; + + source: Maybe<__Source>; + + host: Maybe<_Host>; + }; + + export type __Source = { + __typename?: 'SourceEcsFields'; + + ip: Maybe<string[]>; + }; + + export type _Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetLastEventTimeQuery { + export type Variables = { + sourceId: string; + indexKey: LastEventIndexKey; + details: LastTimeDetails; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + LastEventTime: LastEventTime; + }; + + export type LastEventTime = { + __typename?: 'LastEventTimeData'; + + lastSeen: Maybe<string>; + }; +} + +export namespace GetHostFirstLastSeenQuery { + export type Variables = { + sourceId: string; + hostName: string; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + HostFirstLastSeen: HostFirstLastSeen; + }; + + export type HostFirstLastSeen = { + __typename?: 'FirstLastSeenHost'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + }; +} + +export namespace GetHostsTableQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + sort: HostsSortField; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Hosts: Hosts; + }; + + export type Hosts = { + __typename?: 'HostsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'HostsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'HostItem'; + + _id: Maybe<string>; + + lastSeen: Maybe<string>; + + host: Maybe<Host>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + name: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetHostOverviewQuery { + export type Variables = { + sourceId: string; + hostName: string; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + HostOverview: HostOverview; + }; + + export type HostOverview = { + __typename?: 'HostItem'; + + _id: Maybe<string>; + + host: Maybe<Host>; + + cloud: Maybe<Cloud>; + + inspect: Maybe<Inspect>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + architecture: Maybe<string[]>; + + id: Maybe<string[]>; + + ip: Maybe<string[]>; + + mac: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + + type: Maybe<string[]>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + family: Maybe<string[]>; + + name: Maybe<string[]>; + + platform: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Cloud = { + __typename?: 'CloudFields'; + + instance: Maybe<Instance>; + + machine: Maybe<Machine>; + + provider: Maybe<(Maybe<string>)[]>; + + region: Maybe<(Maybe<string>)[]>; + }; + + export type Instance = { + __typename?: 'CloudInstance'; + + id: Maybe<(Maybe<string>)[]>; + }; + + export type Machine = { + __typename?: 'CloudMachine'; + + type: Maybe<(Maybe<string>)[]>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetIpOverviewQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + ip: string; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + IpOverview: Maybe<IpOverview>; + }; + + export type IpOverview = { + __typename?: 'IpOverviewData'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + host: Host; + + inspect: Maybe<Inspect>; + }; + + export type _Source = { + __typename?: 'Overview'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + + autonomousSystem: AutonomousSystem; + + geo: Geo; + }; + + export type AutonomousSystem = { + __typename?: 'AutonomousSystem'; + + number: Maybe<number>; + + organization: Maybe<Organization>; + }; + + export type Organization = { + __typename?: 'AutonomousSystemOrganization'; + + name: Maybe<string>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + city_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + country_name: Maybe<string[]>; + + location: Maybe<Location>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Location = { + __typename?: 'Location'; + + lat: Maybe<number[]>; + + lon: Maybe<number[]>; + }; + + export type Destination = { + __typename?: 'Overview'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + + autonomousSystem: _AutonomousSystem; + + geo: _Geo; + }; + + export type _AutonomousSystem = { + __typename?: 'AutonomousSystem'; + + number: Maybe<number>; + + organization: Maybe<_Organization>; + }; + + export type _Organization = { + __typename?: 'AutonomousSystemOrganization'; + + name: Maybe<string>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + city_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + country_name: Maybe<string[]>; + + location: Maybe<_Location>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type _Location = { + __typename?: 'Location'; + + lat: Maybe<number[]>; + + lon: Maybe<number[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + architecture: Maybe<string[]>; + + id: Maybe<string[]>; + + ip: Maybe<string[]>; + + mac: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + + type: Maybe<string[]>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + family: Maybe<string[]>; + + name: Maybe<string[]>; + + platform: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiHostDetailsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiHostDetails: KpiHostDetails; + }; + + export type KpiHostDetails = { + __typename?: 'KpiHostDetailsData'; + + authSuccess: Maybe<number>; + + authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; + + authFailure: Maybe<number>; + + authFailureHistogram: Maybe<AuthFailureHistogram[]>; + + uniqueSourceIps: Maybe<number>; + + uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; + + uniqueDestinationIps: Maybe<number>; + + uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; + + inspect: Maybe<Inspect>; + }; + + export type AuthSuccessHistogram = KpiHostDetailsChartFields.Fragment; + + export type AuthFailureHistogram = KpiHostDetailsChartFields.Fragment; + + export type UniqueSourceIpsHistogram = KpiHostDetailsChartFields.Fragment; + + export type UniqueDestinationIpsHistogram = KpiHostDetailsChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiHostsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiHosts: KpiHosts; + }; + + export type KpiHosts = { + __typename?: 'KpiHostsData'; + + hosts: Maybe<number>; + + hostsHistogram: Maybe<HostsHistogram[]>; + + authSuccess: Maybe<number>; + + authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; + + authFailure: Maybe<number>; + + authFailureHistogram: Maybe<AuthFailureHistogram[]>; + + uniqueSourceIps: Maybe<number>; + + uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; + + uniqueDestinationIps: Maybe<number>; + + uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; + + inspect: Maybe<Inspect>; + }; + + export type HostsHistogram = KpiHostChartFields.Fragment; + + export type AuthSuccessHistogram = KpiHostChartFields.Fragment; + + export type AuthFailureHistogram = KpiHostChartFields.Fragment; + + export type UniqueSourceIpsHistogram = KpiHostChartFields.Fragment; + + export type UniqueDestinationIpsHistogram = KpiHostChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiNetworkQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiNetwork: Maybe<KpiNetwork>; + }; + + export type KpiNetwork = { + __typename?: 'KpiNetworkData'; + + networkEvents: Maybe<number>; + + uniqueFlowId: Maybe<number>; + + uniqueSourcePrivateIps: Maybe<number>; + + uniqueSourcePrivateIpsHistogram: Maybe<UniqueSourcePrivateIpsHistogram[]>; + + uniqueDestinationPrivateIps: Maybe<number>; + + uniqueDestinationPrivateIpsHistogram: Maybe<UniqueDestinationPrivateIpsHistogram[]>; + + dnsQueries: Maybe<number>; + + tlsHandshakes: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type UniqueSourcePrivateIpsHistogram = KpiNetworkChartFields.Fragment; + + export type UniqueDestinationPrivateIpsHistogram = KpiNetworkChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetMatrixHistogramQuery { + export type Variables = { + defaultIndex: string[]; + filterQuery?: Maybe<string>; + histogramType: HistogramType; + inspect: boolean; + sourceId: string; + stackByField: string; + timerange: TimerangeInput; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + MatrixHistogram: MatrixHistogram; + }; + + export type MatrixHistogram = { + __typename?: 'MatrixHistogramOverTimeData'; + + matrixHistogramData: MatrixHistogramData[]; + + totalCount: number; + + inspect: Maybe<Inspect>; + }; + + export type MatrixHistogramData = { + __typename?: 'MatrixOverTimeHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + + g: Maybe<string>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkDnsQuery { + export type Variables = { + defaultIndex: string[]; + filterQuery?: Maybe<string>; + inspect: boolean; + isPtrIncluded: boolean; + pagination: PaginationInputPaginated; + sort: NetworkDnsSortField; + sourceId: string; + stackByField?: Maybe<string>; + timerange: TimerangeInput; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkDns: NetworkDns; + }; + + export type NetworkDns = { + __typename?: 'NetworkDnsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkDnsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkDnsItem'; + + _id: Maybe<string>; + + dnsBytesIn: Maybe<number>; + + dnsBytesOut: Maybe<number>; + + dnsName: Maybe<string>; + + queryCount: Maybe<number>; + + uniqueDomains: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkHttpQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkHttpSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkHttp: NetworkHttp; + }; + + export type NetworkHttp = { + __typename?: 'NetworkHttpData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkHttpEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkHttpItem'; + + domains: string[]; + + lastHost: Maybe<string>; + + lastSourceIp: Maybe<string>; + + methods: string[]; + + path: Maybe<string>; + + requestCount: Maybe<number>; + + statuses: string[]; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkTopCountriesQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkTopCountries: NetworkTopCountries; + }; + + export type NetworkTopCountries = { + __typename?: 'NetworkTopCountriesData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkTopCountriesEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkTopCountriesItem'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + network: Maybe<Network>; + }; + + export type _Source = { + __typename?: 'TopCountriesItemSource'; + + country: Maybe<string>; + + destination_ips: Maybe<number>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type Destination = { + __typename?: 'TopCountriesItemDestination'; + + country: Maybe<string>; + + destination_ips: Maybe<number>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type Network = { + __typename?: 'TopNetworkTablesEcsField'; + + bytes_in: Maybe<number>; + + bytes_out: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkTopNFlowQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkTopNFlow: NetworkTopNFlow; + }; + + export type NetworkTopNFlow = { + __typename?: 'NetworkTopNFlowData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkTopNFlowEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkTopNFlowItem'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + network: Maybe<Network>; + }; + + export type _Source = { + __typename?: 'TopNFlowItemSource'; + + autonomous_system: Maybe<AutonomousSystem>; + + domain: Maybe<string[]>; + + ip: Maybe<string>; + + location: Maybe<Location>; + + flows: Maybe<number>; + + destination_ips: Maybe<number>; + }; + + export type AutonomousSystem = { + __typename?: 'AutonomousSystemItem'; + + name: Maybe<string>; + + number: Maybe<number>; + }; + + export type Location = { + __typename?: 'GeoItem'; + + geo: Maybe<Geo>; + + flowTarget: Maybe<FlowTargetSourceDest>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Destination = { + __typename?: 'TopNFlowItemDestination'; + + autonomous_system: Maybe<_AutonomousSystem>; + + domain: Maybe<string[]>; + + ip: Maybe<string>; + + location: Maybe<_Location>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type _AutonomousSystem = { + __typename?: 'AutonomousSystemItem'; + + name: Maybe<string>; + + number: Maybe<number>; + }; + + export type _Location = { + __typename?: 'GeoItem'; + + geo: Maybe<_Geo>; + + flowTarget: Maybe<FlowTargetSourceDest>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Network = { + __typename?: 'TopNetworkTablesEcsField'; + + bytes_in: Maybe<number>; + + bytes_out: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetOverviewHostQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + OverviewHost: Maybe<OverviewHost>; + }; + + export type OverviewHost = { + __typename?: 'OverviewHostData'; + + auditbeatAuditd: Maybe<number>; + + auditbeatFIM: Maybe<number>; + + auditbeatLogin: Maybe<number>; + + auditbeatPackage: Maybe<number>; + + auditbeatProcess: Maybe<number>; + + auditbeatUser: Maybe<number>; + + endgameDns: Maybe<number>; + + endgameFile: Maybe<number>; + + endgameImageLoad: Maybe<number>; + + endgameNetwork: Maybe<number>; + + endgameProcess: Maybe<number>; + + endgameRegistry: Maybe<number>; + + endgameSecurity: Maybe<number>; + + filebeatSystemModule: Maybe<number>; + + winlogbeatSecurity: Maybe<number>; + + winlogbeatMWSysmonOperational: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetOverviewNetworkQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + OverviewNetwork: Maybe<OverviewNetwork>; + }; + + export type OverviewNetwork = { + __typename?: 'OverviewNetworkData'; + + auditbeatSocket: Maybe<number>; + + filebeatCisco: Maybe<number>; + + filebeatNetflow: Maybe<number>; + + filebeatPanw: Maybe<number>; + + filebeatSuricata: Maybe<number>; + + filebeatZeek: Maybe<number>; + + packetbeatDNS: Maybe<number>; + + packetbeatFlow: Maybe<number>; + + packetbeatTLS: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace SourceQuery { + export type Variables = { + sourceId?: Maybe<string>; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + status: Status; + }; + + export type Status = { + __typename?: 'SourceStatus'; + + indicesExist: boolean; + + indexFields: IndexFields[]; + }; + + export type IndexFields = { + __typename?: 'IndexField'; + + category: string; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: (Maybe<string>)[]; + + name: string; + + searchable: boolean; + + type: string; + + aggregatable: boolean; + + format: Maybe<string>; + }; +} + +export namespace GetAllTimeline { + export type Variables = { + pageInfo: PageInfoTimeline; + search?: Maybe<string>; + sort?: Maybe<SortTimeline>; + onlyUserFavorite?: Maybe<boolean>; + }; + + export type Query = { + __typename?: 'Query'; + + getAllTimeline: GetAllTimeline; + }; + + export type GetAllTimeline = { + __typename?: 'ResponseTimelines'; + + totalCount: Maybe<number>; + + timeline: (Maybe<Timeline>)[]; + }; + + export type Timeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + description: Maybe<string>; + + favorite: Maybe<Favorite[]>; + + eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; + + notes: Maybe<Notes[]>; + + noteIds: Maybe<string[]>; + + pinnedEventIds: Maybe<string[]>; + + title: Maybe<string>; + + timelineType: Maybe<TimelineType>; + + templateTimelineId: Maybe<string>; + + templateTimelineVersion: Maybe<number>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: string; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type EventIdToNoteIds = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + timelineVersion: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Notes = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace DeleteTimelineMutation { + export type Variables = { + id: string[]; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + deleteTimeline: boolean; + }; +} + +export namespace GetTimelineDetailsQuery { + export type Variables = { + sourceId: string; + eventId: string; + indexName: string; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + TimelineDetails: TimelineDetails; + }; + + export type TimelineDetails = { + __typename?: 'TimelineDetailsData'; + + data: Maybe<Data[]>; + }; + + export type Data = { + __typename?: 'DetailItem'; + + field: string; + + values: Maybe<string[]>; + + originalValue: Maybe<EsValue>; + }; +} + +export namespace PersistTimelineFavoriteMutation { + export type Variables = { + timelineId?: Maybe<string>; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistFavorite: PersistFavorite; + }; + + export type PersistFavorite = { + __typename?: 'ResponseFavoriteTimeline'; + + savedObjectId: string; + + version: string; + + favorite: Maybe<Favorite[]>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; +} + +export namespace GetTimelineQuery { + export type Variables = { + sourceId: string; + fieldRequested: string[]; + pagination: PaginationInput; + sortField: SortField; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Timeline: Timeline; + }; + + export type Timeline = { + __typename?: 'TimelineData'; + + totalCount: number; + + inspect: Maybe<Inspect>; + + pageInfo: PageInfo; + + edges: Edges[]; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; + + export type PageInfo = { + __typename?: 'PageInfo'; + + endCursor: Maybe<EndCursor>; + + hasNextPage: Maybe<boolean>; + }; + + export type EndCursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + + tiebreaker: Maybe<string>; + }; + + export type Edges = { + __typename?: 'TimelineEdges'; + + node: Node; + }; + + export type Node = { + __typename?: 'TimelineItem'; + + _id: string; + + _index: Maybe<string>; + + data: Data[]; + + ecs: Ecs; + }; + + export type Data = { + __typename?: 'TimelineNonEcsData'; + + field: string; + + value: Maybe<string[]>; + }; + + export type Ecs = { + __typename?: 'ECS'; + + _id: string; + + _index: Maybe<string>; + + timestamp: Maybe<string>; + + message: Maybe<string[]>; + + system: Maybe<System>; + + event: Maybe<Event>; + + auditd: Maybe<Auditd>; + + file: Maybe<File>; + + host: Maybe<Host>; + + rule: Maybe<Rule>; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + dns: Maybe<Dns>; + + endgame: Maybe<Endgame>; + + geo: Maybe<__Geo>; + + signal: Maybe<Signal>; + + suricata: Maybe<Suricata>; + + network: Maybe<Network>; + + http: Maybe<Http>; + + tls: Maybe<Tls>; + + url: Maybe<Url>; + + user: Maybe<User>; + + winlog: Maybe<Winlog>; + + process: Maybe<Process>; + + zeek: Maybe<Zeek>; + }; + + export type System = { + __typename?: 'SystemEcsField'; + + auth: Maybe<Auth>; + + audit: Maybe<Audit>; + }; + + export type Auth = { + __typename?: 'AuthEcsFields'; + + ssh: Maybe<Ssh>; + }; + + export type Ssh = { + __typename?: 'SshEcsFields'; + + signature: Maybe<string[]>; + + method: Maybe<string[]>; + }; + + export type Audit = { + __typename?: 'AuditEcsFields'; + + package: Maybe<Package>; + }; + + export type Package = { + __typename?: 'PackageEcsFields'; + + arch: Maybe<string[]>; + + entity_id: Maybe<string[]>; + + name: Maybe<string[]>; + + size: Maybe<number[]>; + + summary: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Event = { + __typename?: 'EventEcsFields'; + + action: Maybe<string[]>; + + category: Maybe<string[]>; + + code: Maybe<string[]>; + + created: Maybe<string[]>; + + dataset: Maybe<string[]>; + + duration: Maybe<number[]>; + + end: Maybe<string[]>; + + hash: Maybe<string[]>; + + id: Maybe<string[]>; + + kind: Maybe<string[]>; + + module: Maybe<string[]>; + + original: Maybe<string[]>; + + outcome: Maybe<string[]>; + + risk_score: Maybe<number[]>; + + risk_score_norm: Maybe<number[]>; + + severity: Maybe<number[]>; + + start: Maybe<string[]>; + + timezone: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type Auditd = { + __typename?: 'AuditdEcsFields'; + + result: Maybe<string[]>; + + session: Maybe<string[]>; + + data: Maybe<_Data>; + + summary: Maybe<Summary>; + }; + + export type _Data = { + __typename?: 'AuditdData'; + + acct: Maybe<string[]>; + + terminal: Maybe<string[]>; + + op: Maybe<string[]>; + }; + + export type Summary = { + __typename?: 'Summary'; + + actor: Maybe<Actor>; + + object: Maybe<Object>; + + how: Maybe<string[]>; + + message_type: Maybe<string[]>; + + sequence: Maybe<string[]>; + }; + + export type Actor = { + __typename?: 'PrimarySecondary'; + + primary: Maybe<string[]>; + + secondary: Maybe<string[]>; + }; + + export type Object = { + __typename?: 'PrimarySecondary'; + + primary: Maybe<string[]>; + + secondary: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type File = { + __typename?: 'FileFields'; + + name: Maybe<string[]>; + + path: Maybe<string[]>; + + target_path: Maybe<string[]>; + + extension: Maybe<string[]>; + + type: Maybe<string[]>; + + device: Maybe<string[]>; + + inode: Maybe<string[]>; + + uid: Maybe<string[]>; + + owner: Maybe<string[]>; + + gid: Maybe<string[]>; + + group: Maybe<string[]>; + + mode: Maybe<string[]>; + + size: Maybe<number[]>; + + mtime: Maybe<string[]>; + + ctime: Maybe<string[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + + ip: Maybe<string[]>; + }; + + export type Rule = { + __typename?: 'RuleEcsField'; + + reference: Maybe<string[]>; + }; + + export type _Source = { + __typename?: 'SourceEcsFields'; + + bytes: Maybe<number[]>; + + ip: Maybe<string[]>; + + packets: Maybe<number[]>; + + port: Maybe<number[]>; + + geo: Maybe<Geo>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Destination = { + __typename?: 'DestinationEcsFields'; + + bytes: Maybe<number[]>; + + ip: Maybe<string[]>; + + packets: Maybe<number[]>; + + port: Maybe<number[]>; + + geo: Maybe<_Geo>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Dns = { + __typename?: 'DnsEcsFields'; + + question: Maybe<Question>; + + resolved_ip: Maybe<string[]>; + + response_code: Maybe<string[]>; + }; + + export type Question = { + __typename?: 'DnsQuestionData'; + + name: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type Endgame = { + __typename?: 'EndgameEcsFields'; + + exit_code: Maybe<number[]>; + + file_name: Maybe<string[]>; + + file_path: Maybe<string[]>; + + logon_type: Maybe<number[]>; + + parent_process_name: Maybe<string[]>; + + pid: Maybe<number[]>; + + process_name: Maybe<string[]>; + + subject_domain_name: Maybe<string[]>; + + subject_logon_id: Maybe<string[]>; + + subject_user_name: Maybe<string[]>; + + target_domain_name: Maybe<string[]>; + + target_logon_id: Maybe<string[]>; + + target_user_name: Maybe<string[]>; + }; + + export type __Geo = { + __typename?: 'GeoEcsFields'; + + region_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + }; + + export type Signal = { + __typename?: 'SignalField'; + + original_time: Maybe<string[]>; + + rule: Maybe<_Rule>; + }; + + export type _Rule = { + __typename?: 'RuleField'; + + id: Maybe<string[]>; + + saved_id: Maybe<string[]>; + + timeline_id: Maybe<string[]>; + + timeline_title: Maybe<string[]>; + + output_index: Maybe<string[]>; + + from: Maybe<string[]>; + + index: Maybe<string[]>; + + language: Maybe<string[]>; + + query: Maybe<string[]>; + + to: Maybe<string[]>; + + filters: Maybe<ToAny>; + + note: Maybe<string[]>; + }; + + export type Suricata = { + __typename?: 'SuricataEcsFields'; + + eve: Maybe<Eve>; + }; + + export type Eve = { + __typename?: 'SuricataEveData'; + + proto: Maybe<string[]>; + + flow_id: Maybe<number[]>; + + alert: Maybe<Alert>; + }; + + export type Alert = { + __typename?: 'SuricataAlertData'; + + signature: Maybe<string[]>; + + signature_id: Maybe<number[]>; + }; + + export type Network = { + __typename?: 'NetworkEcsField'; + + bytes: Maybe<number[]>; + + community_id: Maybe<string[]>; + + direction: Maybe<string[]>; + + packets: Maybe<number[]>; + + protocol: Maybe<string[]>; + + transport: Maybe<string[]>; + }; + + export type Http = { + __typename?: 'HttpEcsFields'; + + version: Maybe<string[]>; + + request: Maybe<Request>; + + response: Maybe<Response>; + }; + + export type Request = { + __typename?: 'HttpRequestData'; + + method: Maybe<string[]>; + + body: Maybe<Body>; + + referrer: Maybe<string[]>; + }; + + export type Body = { + __typename?: 'HttpBodyData'; + + bytes: Maybe<number[]>; + + content: Maybe<string[]>; + }; + + export type Response = { + __typename?: 'HttpResponseData'; + + status_code: Maybe<number[]>; + + body: Maybe<_Body>; + }; + + export type _Body = { + __typename?: 'HttpBodyData'; + + bytes: Maybe<number[]>; + + content: Maybe<string[]>; + }; + + export type Tls = { + __typename?: 'TlsEcsFields'; + + client_certificate: Maybe<ClientCertificate>; + + fingerprints: Maybe<Fingerprints>; + + server_certificate: Maybe<ServerCertificate>; + }; + + export type ClientCertificate = { + __typename?: 'TlsClientCertificateData'; + + fingerprint: Maybe<Fingerprint>; + }; + + export type Fingerprint = { + __typename?: 'FingerprintData'; + + sha1: Maybe<string[]>; + }; + + export type Fingerprints = { + __typename?: 'TlsFingerprintsData'; + + ja3: Maybe<Ja3>; + }; + + export type Ja3 = { + __typename?: 'TlsJa3Data'; + + hash: Maybe<string[]>; + }; + + export type ServerCertificate = { + __typename?: 'TlsServerCertificateData'; + + fingerprint: Maybe<_Fingerprint>; + }; + + export type _Fingerprint = { + __typename?: 'FingerprintData'; + + sha1: Maybe<string[]>; + }; + + export type Url = { + __typename?: 'UrlEcsFields'; + + original: Maybe<string[]>; + + domain: Maybe<string[]>; + + username: Maybe<string[]>; + + password: Maybe<string[]>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + domain: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Winlog = { + __typename?: 'WinlogEcsFields'; + + event_id: Maybe<number[]>; + }; + + export type Process = { + __typename?: 'ProcessEcsFields'; + + hash: Maybe<Hash>; + + pid: Maybe<number[]>; + + name: Maybe<string[]>; + + ppid: Maybe<number[]>; + + args: Maybe<string[]>; + + executable: Maybe<string[]>; + + title: Maybe<string[]>; + + working_directory: Maybe<string[]>; + }; + + export type Hash = { + __typename?: 'ProcessHashData'; + + md5: Maybe<string[]>; + + sha1: Maybe<string[]>; + + sha256: Maybe<string[]>; + }; + + export type Zeek = { + __typename?: 'ZeekEcsFields'; + + session_id: Maybe<string[]>; + + connection: Maybe<Connection>; + + notice: Maybe<Notice>; + + dns: Maybe<_Dns>; + + http: Maybe<_Http>; + + files: Maybe<Files>; + + ssl: Maybe<Ssl>; + }; + + export type Connection = { + __typename?: 'ZeekConnectionData'; + + local_resp: Maybe<boolean[]>; + + local_orig: Maybe<boolean[]>; + + missed_bytes: Maybe<number[]>; + + state: Maybe<string[]>; + + history: Maybe<string[]>; + }; + + export type Notice = { + __typename?: 'ZeekNoticeData'; + + suppress_for: Maybe<number[]>; + + msg: Maybe<string[]>; + + note: Maybe<string[]>; + + sub: Maybe<string[]>; + + dst: Maybe<string[]>; + + dropped: Maybe<boolean[]>; + + peer_descr: Maybe<string[]>; + }; + + export type _Dns = { + __typename?: 'ZeekDnsData'; + + AA: Maybe<boolean[]>; + + qclass_name: Maybe<string[]>; + + RD: Maybe<boolean[]>; + + qtype_name: Maybe<string[]>; + + rejected: Maybe<boolean[]>; + + qtype: Maybe<string[]>; + + query: Maybe<string[]>; + + trans_id: Maybe<number[]>; + + qclass: Maybe<string[]>; + + RA: Maybe<boolean[]>; + + TC: Maybe<boolean[]>; + }; + + export type _Http = { + __typename?: 'ZeekHttpData'; + + resp_mime_types: Maybe<string[]>; + + trans_depth: Maybe<string[]>; + + status_msg: Maybe<string[]>; + + resp_fuids: Maybe<string[]>; + + tags: Maybe<string[]>; + }; + + export type Files = { + __typename?: 'ZeekFileData'; + + session_ids: Maybe<string[]>; + + timedout: Maybe<boolean[]>; + + local_orig: Maybe<boolean[]>; + + tx_host: Maybe<string[]>; + + source: Maybe<string[]>; + + is_orig: Maybe<boolean[]>; + + overflow_bytes: Maybe<number[]>; + + sha1: Maybe<string[]>; + + duration: Maybe<number[]>; + + depth: Maybe<number[]>; + + analyzers: Maybe<string[]>; + + mime_type: Maybe<string[]>; + + rx_host: Maybe<string[]>; + + total_bytes: Maybe<number[]>; + + fuid: Maybe<string[]>; + + seen_bytes: Maybe<number[]>; + + missing_bytes: Maybe<number[]>; + + md5: Maybe<string[]>; + }; + + export type Ssl = { + __typename?: 'ZeekSslData'; + + cipher: Maybe<string[]>; + + established: Maybe<boolean[]>; + + resumed: Maybe<boolean[]>; + + version: Maybe<string[]>; + }; +} + +export namespace PersistTimelineNoteMutation { + export type Variables = { + noteId?: Maybe<string>; + version?: Maybe<string>; + note: NoteInput; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistNote: PersistNote; + }; + + export type PersistNote = { + __typename?: 'ResponseNote'; + + code: Maybe<number>; + + message: Maybe<string>; + + note: Note; + }; + + export type Note = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace GetOneTimeline { + export type Variables = { + id: string; + }; + + export type Query = { + __typename?: 'Query'; + + getOneTimeline: GetOneTimeline; + }; + + export type GetOneTimeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + columns: Maybe<Columns[]>; + + dataProviders: Maybe<DataProviders[]>; + + dateRange: Maybe<DateRange>; + + description: Maybe<string>; + + eventType: Maybe<string>; + + eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; + + favorite: Maybe<Favorite[]>; + + filters: Maybe<Filters[]>; + + kqlMode: Maybe<string>; + + kqlQuery: Maybe<KqlQuery>; + + notes: Maybe<Notes[]>; + + noteIds: Maybe<string[]>; + + pinnedEventIds: Maybe<string[]>; + + pinnedEventsSaveObject: Maybe<PinnedEventsSaveObject[]>; + + title: Maybe<string>; + + savedQueryId: Maybe<string>; + + sort: Maybe<Sort>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: string; + }; + + export type Columns = { + __typename?: 'ColumnHeaderResult'; + + aggregatable: Maybe<boolean>; + + category: Maybe<string>; + + columnHeaderType: Maybe<string>; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: Maybe<string[]>; + + id: Maybe<string>; + + name: Maybe<string>; + + searchable: Maybe<boolean>; + + type: Maybe<string>; + }; + + export type DataProviders = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<QueryMatch>; + + and: Maybe<And[]>; + }; + + export type QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type And = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<_QueryMatch>; + }; + + export type _QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type DateRange = { + __typename?: 'DateRangePickerResult'; + + start: Maybe<number>; + + end: Maybe<number>; + }; + + export type EventIdToNoteIds = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + timelineVersion: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe<Meta>; + + query: Maybe<string>; + + exists: Maybe<string>; + + match_all: Maybe<string>; + + missing: Maybe<string>; + + range: Maybe<string>; + + script: Maybe<string>; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe<string>; + + controlledBy: Maybe<string>; + + disabled: Maybe<boolean>; + + field: Maybe<string>; + + formattedValue: Maybe<string>; + + index: Maybe<string>; + + key: Maybe<string>; + + negate: Maybe<boolean>; + + params: Maybe<string>; + + type: Maybe<string>; + + value: Maybe<string>; + }; + + export type KqlQuery = { + __typename?: 'SerializedFilterQueryResult'; + + filterQuery: Maybe<FilterQuery>; + }; + + export type FilterQuery = { + __typename?: 'SerializedKueryQueryResult'; + + kuery: Maybe<Kuery>; + + serializedQuery: Maybe<string>; + }; + + export type Kuery = { + __typename?: 'KueryFilterQueryResult'; + + kind: Maybe<string>; + + expression: Maybe<string>; + }; + + export type Notes = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type PinnedEventsSaveObject = { + __typename?: 'PinnedEvent'; + + pinnedEventId: string; + + eventId: Maybe<string>; + + timelineId: Maybe<string>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Sort = { + __typename?: 'SortTimelineResult'; + + columnId: Maybe<string>; + + sortDirection: Maybe<string>; + }; +} + +export namespace PersistTimelineMutation { + export type Variables = { + timelineId?: Maybe<string>; + version?: Maybe<string>; + timeline: TimelineInput; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistTimeline: PersistTimeline; + }; + + export type PersistTimeline = { + __typename?: 'ResponseTimeline'; + + code: Maybe<number>; + + message: Maybe<string>; + + timeline: Timeline; + }; + + export type Timeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + version: string; + + columns: Maybe<Columns[]>; + + dataProviders: Maybe<DataProviders[]>; + + description: Maybe<string>; + + eventType: Maybe<string>; + + favorite: Maybe<Favorite[]>; + + filters: Maybe<Filters[]>; + + kqlMode: Maybe<string>; + + kqlQuery: Maybe<KqlQuery>; + + title: Maybe<string>; + + dateRange: Maybe<DateRange>; + + savedQueryId: Maybe<string>; + + sort: Maybe<Sort>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + }; + + export type Columns = { + __typename?: 'ColumnHeaderResult'; + + aggregatable: Maybe<boolean>; + + category: Maybe<string>; + + columnHeaderType: Maybe<string>; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: Maybe<string[]>; + + id: Maybe<string>; + + name: Maybe<string>; + + searchable: Maybe<boolean>; + + type: Maybe<string>; + }; + + export type DataProviders = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<QueryMatch>; + + and: Maybe<And[]>; + }; + + export type QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type And = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<_QueryMatch>; + }; + + export type _QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe<Meta>; + + query: Maybe<string>; + + exists: Maybe<string>; + + match_all: Maybe<string>; + + missing: Maybe<string>; + + range: Maybe<string>; + + script: Maybe<string>; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe<string>; + + controlledBy: Maybe<string>; + + disabled: Maybe<boolean>; + + field: Maybe<string>; + + formattedValue: Maybe<string>; + + index: Maybe<string>; + + key: Maybe<string>; + + negate: Maybe<boolean>; + + params: Maybe<string>; + + type: Maybe<string>; + + value: Maybe<string>; + }; + + export type KqlQuery = { + __typename?: 'SerializedFilterQueryResult'; + + filterQuery: Maybe<FilterQuery>; + }; + + export type FilterQuery = { + __typename?: 'SerializedKueryQueryResult'; + + kuery: Maybe<Kuery>; + + serializedQuery: Maybe<string>; + }; + + export type Kuery = { + __typename?: 'KueryFilterQueryResult'; + + kind: Maybe<string>; + + expression: Maybe<string>; + }; + + export type DateRange = { + __typename?: 'DateRangePickerResult'; + + start: Maybe<number>; + + end: Maybe<number>; + }; + + export type Sort = { + __typename?: 'SortTimelineResult'; + + columnId: Maybe<string>; + + sortDirection: Maybe<string>; + }; +} + +export namespace PersistTimelinePinnedEventMutation { + export type Variables = { + pinnedEventId?: Maybe<string>; + eventId: string; + timelineId?: Maybe<string>; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistPinnedEventOnTimeline: Maybe<PersistPinnedEventOnTimeline>; + }; + + export type PersistPinnedEventOnTimeline = { + __typename?: 'PinnedEvent'; + + pinnedEventId: string; + + eventId: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace GetTlsQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + flowTarget: FlowTargetSourceDest; + ip: string; + pagination: PaginationInputPaginated; + sort: TlsSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Tls: Tls; + }; + + export type Tls = { + __typename?: 'TlsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'TlsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'TlsNode'; + + _id: Maybe<string>; + + subjects: Maybe<string[]>; + + ja3: Maybe<string[]>; + + issuers: Maybe<string[]>; + + notAfter: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetUncommonProcessesQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + UncommonProcesses: UncommonProcesses; + }; + + export type UncommonProcesses = { + __typename?: 'UncommonProcessesData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'UncommonProcessesEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'UncommonProcessItem'; + + _id: string; + + instances: number; + + process: Process; + + user: Maybe<User>; + + hosts: Hosts[]; + }; + + export type Process = { + __typename?: 'ProcessEcsFields'; + + args: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Hosts = { + __typename?: 'HostEcsFields'; + + name: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetUsersQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + flowTarget: FlowTarget; + ip: string; + pagination: PaginationInputPaginated; + sort: UsersSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Users: Users; + }; + + export type Users = { + __typename?: 'UsersData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'UsersEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'UsersNode'; + + user: Maybe<User>; + }; + + export type User = { + __typename?: 'UsersItem'; + + name: Maybe<string>; + + id: Maybe<string[]>; + + groupId: Maybe<string[]>; + + groupName: Maybe<string[]>; + + count: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace KpiHostDetailsChartFields { + export type Fragment = { + __typename?: 'KpiHostHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} + +export namespace KpiHostChartFields { + export type Fragment = { + __typename?: 'KpiHostHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} + +export namespace KpiNetworkChartFields { + export type Fragment = { + __typename?: 'KpiNetworkHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx rename to x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/plugins/siem/public/hooks/api/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/api.tsx rename to x-pack/plugins/siem/public/hooks/api/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx b/x-pack/plugins/siem/public/hooks/api/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx rename to x-pack/plugins/siem/public/hooks/api/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx b/x-pack/plugins/siem/public/hooks/api/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx rename to x-pack/plugins/siem/public/hooks/api/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/translations.ts b/x-pack/plugins/siem/public/hooks/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/translations.ts rename to x-pack/plugins/siem/public/hooks/translations.ts diff --git a/x-pack/plugins/siem/public/hooks/types.ts b/x-pack/plugins/siem/public/hooks/types.ts new file mode 100644 index 0000000000000..6527904964d00 --- /dev/null +++ b/x-pack/plugins/siem/public/hooks/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SimpleSavedObject } from '../../../../../src/core/public'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject<IndexPatternSavedObjectAttributes>, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/plugins/siem/public/hooks/use_index_patterns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx rename to x-pack/plugins/siem/public/hooks/use_index_patterns.tsx diff --git a/x-pack/plugins/siem/public/index.ts b/x-pack/plugins/siem/public/index.ts new file mode 100644 index 0000000000000..46f72c1fa17c8 --- /dev/null +++ b/x-pack/plugins/siem/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/public'; +import { Plugin, PluginSetup, PluginStart } from './plugin'; + +export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); + +export { Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx b/x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx rename to x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/translations.ts b/x-pack/plugins/siem/public/lib/clipboard/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/translations.ts rename to x-pack/plugins/siem/public/lib/clipboard/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx rename to x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.test.ts b/x-pack/plugins/siem/public/lib/compose/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/compose/helpers.test.ts rename to x-pack/plugins/siem/public/lib/compose/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts b/x-pack/plugins/siem/public/lib/compose/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts rename to x-pack/plugins/siem/public/lib/compose/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx b/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx rename to x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx index c742ced4c504c..fb30c9a5411ed 100644 --- a/x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx +++ b/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx @@ -8,8 +8,8 @@ import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemo import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; +import { CoreStart } from '../../../../../../src/core/public'; import introspectionQueryResultData from '../../graphql/introspection.json'; -import { CoreStart } from '../../plugin'; import { AppFrontendLibs } from '../lib'; import { getLinks } from './helpers'; diff --git a/x-pack/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 536798ffad41b..9fe0b4a957ceb 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 0000000000000..2def4b5107aee --- /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 0000000000000..810baa89cd60d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/keury/index.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, isString, flow } from 'lodash/fp'; +import { + EsQueryConfig, + Query, + Filter, + esQuery, + esKuery, + IIndexPattern, +} from '../../../../../../src/plugins/data/public'; + +import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; + +import { KueryFilterQuery } from '../../store'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern?: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; + +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: IIndexPattern +): JsonObject => { + try { + return kueryExpression + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + +export const escapeQueryValue = (val: number | string = ''): string | number => { + if (isString(val)) { + if (isEmpty(val)) { + return '""'; + } + return `"${escapeKuery(val)}"`; + } + + return val; +}; + +export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { + if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { + try { + esKuery.fromKueryExpression(kqlFilterQuery.expression); + } catch (err) { + return false; + } + } + return true; +}; + +const escapeWhitespace = (val: string) => + val + .replace(/\t/g, '\\t') + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n'); + +// See the SpecialCharacter rule in kuery.peg +const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string + +// See the Keyword rule in kuery.peg +const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); + +const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); + +export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); + +export const convertToBuildEsQuery = ({ + config, + indexPattern, + queries, + filters, +}: { + config: EsQueryConfig; + indexPattern: IIndexPattern; + queries: Query[]; + filters: Filter[]; +}) => { + try { + return JSON.stringify( + esQuery.buildEsQuery( + indexPattern, + queries, + filters.filter(f => f.meta.disabled === false), + { + ...config, + dateFormatTZ: undefined, + } + ) + ); + } catch (exp) { + return ''; + } +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts b/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts new file mode 100644 index 0000000000000..c3e1f35f37356 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createKibanaContextProviderMock, + createUseUiSettingMock, + createUseUiSetting$Mock, + createUseKibanaMock, + createWithKibanaMock, +} from '../../../mock/kibana_react'; + +export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const useKibana = jest.fn(createUseKibanaMock()); +export const useUiSetting = jest.fn(createUseUiSettingMock()); +export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); +export const useTimeZone = jest.fn(); +export const useDateFormat = jest.fn(); +export const useBasePath = jest.fn(() => '/test/base/path'); +export const useCurrentUser = jest.fn(); +export const withKibana = jest.fn(createWithKibanaMock()); +export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts b/x-pack/plugins/siem/public/lib/kibana/hooks.ts similarity index 94% rename from x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts rename to x-pack/plugins/siem/public/lib/kibana/hooks.ts index e1d0a445bf2fb..d62701fe5944a 100644 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts +++ b/x-pack/plugins/siem/public/lib/kibana/hooks.ts @@ -8,13 +8,10 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, -} from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; import { useUiSetting, useKibana } from './kibana_react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { AuthenticatedUser } from '../../../../security/common/model'; import { convertToCamelCase } from '../../containers/case/utils'; export const useDateFormat = (): string => useUiSetting<string>(DEFAULT_DATE_FORMAT); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts b/x-pack/plugins/siem/public/lib/kibana/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/kibana/index.ts rename to x-pack/plugins/siem/public/lib/kibana/index.ts diff --git a/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts b/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts new file mode 100644 index 0000000000000..88be8d25e5840 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + KibanaContextProvider, + KibanaReactContextValue, + useKibana, + useUiSetting, + useUiSetting$, + withKibana, +} from '../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../plugin'; + +export type KibanaContext = KibanaReactContextValue<StartServices>; +export interface WithKibanaProps { + kibana: KibanaContext; +} + +// eslint-disable-next-line react-hooks/rules-of-hooks +const typedUseKibana = () => useKibana<StartServices>(); + +export { + KibanaContextProvider, + typedUseKibana as useKibana, + useUiSetting, + useUiSetting$, + withKibana, +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/services.ts b/x-pack/plugins/siem/public/lib/kibana/services.ts new file mode 100644 index 0000000000000..4ab3e102f56ab --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/services.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from '../../../../../../src/core/public'; + +type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings'>; + +export class KibanaServices { + private static kibanaVersion?: string; + private static services?: GlobalServices; + + public static init({ + http, + kibanaVersion, + uiSettings, + }: GlobalServices & { kibanaVersion: string }) { + this.services = { http, uiSettings }; + this.kibanaVersion = kibanaVersion; + } + + public static get(): GlobalServices { + if (!this.services) { + this.throwUninitializedError(); + } + + return this.services; + } + + public static getKibanaVersion(): string { + if (!this.kibanaVersion) { + this.throwUninitializedError(); + } + + return this.kibanaVersion; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Kibana services not initialized - are you trying to import this module from outside of the SIEM app?' + ); + } +} diff --git a/x-pack/legacy/plugins/siem/public/lib/lib.ts b/x-pack/plugins/siem/public/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/lib.ts rename to x-pack/plugins/siem/public/lib/lib.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/note/index.ts b/x-pack/plugins/siem/public/lib/note/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/note/index.ts rename to x-pack/plugins/siem/public/lib/note/index.ts diff --git a/x-pack/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 59c6cb3566907..ca889e20e695f 100644 --- a/x-pack/legacy/plugins/siem/public/lib/telemetry/middleware.ts +++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts @@ -7,7 +7,7 @@ import { Action, Dispatch, MiddlewareAPI } from 'redux'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from './'; -import { timelineActions } from '../../store/actions'; +import * as timelineActions from '../../store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { diff --git a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx similarity index 86% rename from x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx rename to x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx index b72c34d3b59a7..1696001203bc8 100644 --- a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx +++ b/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx @@ -7,7 +7,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { useUiSetting$ } from '../kibana'; export const useEuiTheme = () => { diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/plugins/siem/public/mock/global_state.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/mock/global_state.ts rename to x-pack/plugins/siem/public/mock/global_state.ts index 266c3aadea8af..6678c3043a3da 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/plugins/siem/public/mock/global_state.ts @@ -22,7 +22,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; export const mockGlobalState: State = { app: { diff --git a/x-pack/legacy/plugins/siem/public/mock/header.ts b/x-pack/plugins/siem/public/mock/header.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/header.ts rename to x-pack/plugins/siem/public/mock/header.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/plugins/siem/public/mock/hook_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx rename to x-pack/plugins/siem/public/mock/hook_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/mock/index.ts b/x-pack/plugins/siem/public/mock/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/index.ts rename to x-pack/plugins/siem/public/mock/index.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/index_pattern.ts b/x-pack/plugins/siem/public/mock/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/index_pattern.ts rename to x-pack/plugins/siem/public/mock/index_pattern.ts diff --git a/x-pack/plugins/siem/public/mock/kibana_core.ts b/x-pack/plugins/siem/public/mock/kibana_core.ts new file mode 100644 index 0000000000000..b175ddbf5106d --- /dev/null +++ b/x-pack/plugins/siem/public/mock/kibana_core.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; + +export const createKibanaCoreStartMock = () => coreMock.createStart(); +export const createKibanaPluginsStartMock = () => ({ + data: dataPluginMock.createStartContract(), +}); diff --git a/x-pack/plugins/siem/public/mock/kibana_react.ts b/x-pack/plugins/siem/public/mock/kibana_react.ts new file mode 100644 index 0000000000000..cebba3e237f98 --- /dev/null +++ b/x-pack/plugins/siem/public/mock/kibana_react.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +import { + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_BYTES_FORMAT, + DEFAULT_INDEX_PATTERN, +} from '../../common/constants'; +import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const mockUiSettings: Record<string, any> = { + [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, + [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, + [DEFAULT_SIEM_TIME_RANGE]: { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }, + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', + [DEFAULT_DATE_FORMAT_TZ]: 'UTC', + [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', + [DEFAULT_DARK_MODE]: false, +}; + +export const createUseUiSettingMock = () => <T extends unknown = string>( + key: string, + defaultValue?: T +): T => { + const result = mockUiSettings[key]; + + if (typeof result != null) return result; + + if (defaultValue != null) { + return defaultValue; + } + + throw new Error(`Unexpected config key: ${key}`); +}; + +export const createUseUiSetting$Mock = () => { + const useUiSettingMock = createUseUiSettingMock(); + + return <T extends unknown = string>( + key: string, + defaultValue?: T + ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; +}; + +export const createUseKibanaMock = () => { + const core = createKibanaCoreStartMock(); + const plugins = createKibanaPluginsStartMock(); + const useUiSetting = createUseUiSettingMock(); + + const services = { + ...core, + ...plugins, + uiSettings: { + ...core.uiSettings, + get: useUiSetting, + }, + }; + + return () => ({ services }); +}; + +export const createWithKibanaMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (Component: any) => (props: any) => { + return React.createElement(Component, { ...props, kibana }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ({ services, ...rest }: any) => + React.createElement(KibanaContextProvider, { + ...rest, + services: { ...kibana.services, ...services }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/public/mock/match_media.ts b/x-pack/plugins/siem/public/mock/match_media.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/match_media.ts rename to x-pack/plugins/siem/public/mock/match_media.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts b/x-pack/plugins/siem/public/mock/mock_detail_item.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts rename to x-pack/plugins/siem/public/mock/mock_detail_item.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts b/x-pack/plugins/siem/public/mock/mock_ecs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts rename to x-pack/plugins/siem/public/mock/mock_ecs.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts b/x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts rename to x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts b/x-pack/plugins/siem/public/mock/mock_timeline_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts rename to x-pack/plugins/siem/public/mock/mock_timeline_data.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/netflow.ts b/x-pack/plugins/siem/public/mock/netflow.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/netflow.ts rename to x-pack/plugins/siem/public/mock/netflow.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/news.ts b/x-pack/plugins/siem/public/mock/news.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/news.ts rename to x-pack/plugins/siem/public/mock/news.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/raw_news.ts b/x-pack/plugins/siem/public/mock/raw_news.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/raw_news.ts rename to x-pack/plugins/siem/public/mock/raw_news.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/plugins/siem/public/mock/test_providers.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/mock/test_providers.tsx rename to x-pack/plugins/siem/public/mock/test_providers.tsx index 952f7f51b63f2..59e3874c6d0a1 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/plugins/siem/public/mock/test_providers.tsx @@ -22,8 +22,6 @@ import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; import { FieldHook, useForm } from '../shared_imports'; -jest.mock('ui/new_platform'); - const state: State = mockGlobalState; interface Props { diff --git a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts similarity index 98% rename from x-pack/legacy/plugins/siem/public/mock/timeline_results.ts rename to x-pack/plugins/siem/public/mock/timeline_results.ts index 363281e563317..edd1c73771829 100644 --- a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -10,7 +10,7 @@ import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query'; import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types'; import { TimelineModel } from '../store/timeline/model'; import { timelineDefaults } from '../store/timeline/defaults'; -import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; +import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter'; export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; @@ -168,6 +168,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 1', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -294,6 +297,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -420,6 +426,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -546,6 +555,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 3', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -672,6 +684,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 4', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -798,6 +813,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 5', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -924,6 +942,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 6', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1050,6 +1071,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1176,6 +1200,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1302,6 +1329,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1428,6 +1458,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1554,6 +1587,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1680,6 +1716,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, diff --git a/x-pack/legacy/plugins/siem/public/mock/utils.ts b/x-pack/plugins/siem/public/mock/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/utils.ts rename to x-pack/plugins/siem/public/mock/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/404.tsx b/x-pack/plugins/siem/public/pages/404.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/404.tsx rename to x-pack/plugins/siem/public/pages/404.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/plugins/siem/public/pages/case/case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/case.tsx rename to x-pack/plugins/siem/public/pages/case/case.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/plugins/siem/public/pages/case/case_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx rename to x-pack/plugins/siem/public/pages/case/case_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts similarity index 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 cc01edcfaab11..12946c3af06bd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -3,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 0000000000000..7ba8ec9666253 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { AddComment } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { wait } from '../../../../lib/helpers'; +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_comment'); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCommentMock = usePostComment as jest.Mock; + +const onCommentSaving = jest.fn(); +const onCommentPosted = jest.fn(); +const postComment = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const addCommentProps = { + caseId: '1234', + disabled: false, + insertQuote: null, + onCommentSaving, + onCommentPosted, + showLoading: false, +}; + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const defaultPostCommment = { + isLoading: false, + isError: false, + postComment, +}; +const sampleData = { + comment: 'what a cool comment', +}; +describe('AddComment ', () => { + const formHookMock = getFormMock(sampleData); + + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCommentMock.mockImplementation(() => defaultPostCommment); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('should post comment on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...addCommentProps} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .simulate('click'); + await wait(); + expect(onCommentSaving).toBeCalled(); + expect(postComment).toBeCalledWith(sampleData, onCommentPosted); + expect(formHookMock.reset).toBeCalled(); + }); + + it('should render spinner and disable submit when loading', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, showLoading: true }} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should disable submit button when disabled prop passed', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, disabled: true }} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should insert a quote if one is available', () => { + const sampleQuote = 'what a cool quote'; + mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, insertQuote: sampleQuote }} /> + </Router> + </TestProviders> + ); + + expect(formHookMock.setFieldValue).toBeCalledWith( + 'comment', + `${sampleData.comment}\n\n${sampleQuote}` + ); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx new file mode 100644 index 0000000000000..aa987b277da06 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import styled from 'styled-components'; + +import { CommentRequest } from '../../../../../../case/common/api'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { Case } from '../../../../containers/case/types'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Form, useForm, UseField } from '../../../../shared_imports'; + +import * as i18n from '../../translations'; +import { schema } from './schema'; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +const initialCommentValue: CommentRequest = { + comment: '', +}; + +interface AddCommentProps { + caseId: string; + disabled?: boolean; + insertQuote: string | null; + onCommentSaving?: () => void; + onCommentPosted: (newCase: Case) => void; + showLoading?: boolean; +} + +export const AddComment = React.memo<AddCommentProps>( + ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm<CommentRequest>({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>( + form, + 'comment' + ); + + useEffect(() => { + if (insertQuote !== null) { + const { comment } = form.getFormData(); + form.setFieldValue( + 'comment', + `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` + ); + } + }, [insertQuote]); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + await postComment(data, onCommentPosted); + form.reset(); + } + }, [form, onCommentPosted, onCommentSaving]); + return ( + <span id="add-comment-permLink"> + {isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />} + <Form form={form}> + <UseField + path="comment" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseComment', + isDisabled: isLoading, + dataTestSubj: 'add-comment', + placeholder: i18n.ADD_COMMENT_HELP_TEXT, + onCursorPositionUpdate: handleCursorChange, + bottomRightContent: ( + <EuiButton + data-test-subj="submit-comment" + iconType="plusInCircle" + isDisabled={isLoading || disabled} + isLoading={isLoading} + onClick={onSubmit} + size="s" + > + {i18n.ADD_COMMENT} + </EuiButton> + ), + topRightContent: ( + <InsertTimelinePopover + hideUntitled={true} + isDisabled={isLoading} + onTimelineChange={handleOnTimelineChange} + /> + ), + }} + /> + </Form> + </span> + ); + } +); + +AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx new file mode 100644 index 0000000000000..ad73fd71b8e11 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CommentRequest } from '../../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema<CommentRequest> = { + comment: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.COMMENT_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx diff --git a/x-pack/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 0000000000000..0eccd8980ccd2 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Connector } from '../../../../../containers/case/configure/types'; +import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; +import { connectorsMock } from '../../../../../containers/case/configure/mock'; +import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; +import { createUseKibanaMock } from '../../../../../mock/kibana_react'; +export { mapping } from '../../../../../containers/case/configure/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; + +export const connectors: Connector[] = connectorsMock; + +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + currentConfiguration: { + connectorId: 'none', + closureType: 'close-by-user', + connectorName: 'none', + }, + firstLoad: false, + loading: false, + mapping: null, + persistCaseConfigure: jest.fn(), + persistLoading: false, + refetchCaseConfigure: jest.fn(), + setClosureType: jest.fn(), + setConnector: jest.fn(), + setCurrentConfiguration: jest.fn(), + setMapping: jest.fn(), + version: '', +}; + +export const useConnectorsResponse: ReturnConnectors = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const kibanaMockImplementationArgs = { + services: { + ...createUseKibanaMock()().services, + triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx similarity index 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 0000000000000..545eceb0f73a1 --- /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 0000000000000..a970fe895eb71 --- /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 0000000000000..4c2e15ddfa98a --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { Create } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_case'); +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { wait } from '../../../../lib/helpers'; +import { SiemPageName } from '../../../home/types'; +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCaseMock = usePostCase as jest.Mock; + +const postCase = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const sampleTags = ['coke', 'pepsi']; +const sampleData = { + description: 'what a great description', + tags: sampleTags, + title: 'what a cool title', +}; +const defaultPostCase = { + isLoading: false, + isError: false, + caseData: null, + postCase, +}; +describe('Create case', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const fetchTags = jest.fn(); + const formHookMock = getFormMock(sampleData); + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCaseMock.mockImplementation(() => defaultPostCase); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + + it('should post case on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-submit"]`) + .first() + .simulate('click'); + await wait(); + expect(postCase).toBeCalledWith(sampleData); + }); + + it('should redirect to all cases on cancel click', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-cancel"]`) + .first() + .simulate('click'); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); + }); + it('should redirect to new case when caseData is there', () => { + const sampleId = '777777'; + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); + mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( + `/${SiemPageName.case}/${sampleId}` + ); + }); + + it('should render spinner when loading', () => { + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 0000000000000..6731b88572cdd --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; + +import { isEqual } from 'lodash/fp'; +import { CasePostRequest } from '../../../../../../case/common/api'; +import { + Field, + Form, + getUseField, + useForm, + UseField, + FormDataProvider, +} from '../../../../shared_imports'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +export const CommonUseField = getUseField({ component: Field }); + +const ContainerBig = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeXL}; + `} +`; + +const Container = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSize}; + `} +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +const initialCaseValue: CasePostRequest = { + description: '', + tags: [], + title: '', +}; + +export const Create = React.memo(() => { + const { caseData, isLoading, postCase } = usePostCase(); + const [isCancel, setIsCancel] = useState(false); + const { form } = useForm<CasePostRequest>({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CasePostRequest>( + form, + 'description' + ); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + await postCase(data); + } + }, [form]); + + const handleSetIsCancel = useCallback(() => { + setIsCancel(true); + }, []); + + if (caseData != null && caseData.id) { + return <Redirect to={`/${SiemPageName.case}/${caseData.id}`} />; + } + + if (isCancel) { + return <Redirect to={`/${SiemPageName.case}`} />; + } + + return ( + <EuiPanel> + {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + <Form form={form}> + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: false, + disabled: isLoading, + }, + }} + /> + <Container> + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + disabled: isLoading, + options, + noSuggestions: false, + }, + }} + /> + </Container> + <ContainerBig> + <UseField + path="description" + component={MarkdownEditorForm} + componentProps={{ + dataTestSubj: 'caseDescription', + idAria: 'caseDescription', + isDisabled: isLoading, + onCursorPositionUpdate: handleCursorChange, + topRightContent: ( + <InsertTimelinePopover + hideUntitled={true} + isDisabled={isLoading} + onTimelineChange={handleOnTimelineChange} + /> + ), + }} + /> + </ContainerBig> + <FormDataProvider pathsToWatch="tags"> + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + </FormDataProvider> + </Form> + <Container> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={handleSetIsCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="create-case-submit" + fill + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + > + {i18n.CREATE_CASE} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + </EuiPanel> + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 0000000000000..a4e0bb6916531 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasePostRequest } from '../../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +import { OptionalFieldLabel } from './optional_field_label'; +const { emptyField } = fieldValidators; + +export const schemaTags = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, +}; + +export const schema: FormSchema<CasePostRequest> = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.NAME, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + label: i18n.DESCRIPTION, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: schemaTags, +}; diff --git a/x-pack/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 0000000000000..9ddb96a4ed295 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { TagList } from './'; +import { getFormMock } from '../__mock__/form'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); +const onSubmit = jest.fn(); +const defaultProps = { + disabled: false, + isLoading: false, + onSubmit, + tags: [], +}; + +describe('TagList ', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const sampleTags = ['coke', 'pepsi']; + const fetchTags = jest.fn(); + const formHookMock = getFormMock({ tags: sampleTags }); + beforeEach(() => { + jest.resetAllMocks(); + (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + it('Renders no tags, and then edit', () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="edit-tags"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Edit tag on submit', async () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-tags-submit"]`) + .last() + .simulate('click'); + await wait(); + expect(onSubmit).toBeCalledWith(sampleTags); + }); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); + it('Cancels on cancel', async () => { + const props = { + ...defaultProps, + tags: ['pepsi'], + }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeFalsy(); + wrapper + .find(`[data-test-subj="edit-tags-cancel"]`) + .last() + .simulate('click'); + await wait(); + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + }); + }); + it('Renders disabled button', () => { + const props = { ...defaultProps, disabled: true }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + }); +}); diff --git a/x-pack/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 0000000000000..d428d9988ae39 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable react/display-name */ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; +import { TestProviders } from '../../../../mock'; +import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; +import { ClosureType } from '../../../../../../case/common/api/cases'; +import * as i18n from './translations'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getKibanaConfigError, getLicenseError } from './helpers'; +import * as api from '../../../../containers/case/configure/api'; +jest.mock('../../../../containers/case/use_get_action_license'); +jest.mock('../../../../containers/case/use_post_push_to_service'); +jest.mock('../../../../containers/case/configure/api'); + +describe('usePushToService', () => { + const caseId = '12345'; + const updateCase = jest.fn(); + const postPushToService = jest.fn(); + const mockPostPush = { + isLoading: false, + postPushToService, + }; + const closureType: ClosureType = 'close-by-user'; + const mockConnector = { + connectorId: 'c00l', + connectorName: 'name', + }; + const mockCaseConfigure = { + ...mockConnector, + createdAt: 'string', + createdBy: {}, + closureType, + updatedAt: 'string', + updatedBy: {}, + version: 'string', + }; + const getConfigureMock = jest.spyOn(api, 'getCaseConfigure'); + const actionLicense = { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + beforeEach(() => { + jest.resetAllMocks(); + (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense, + })); + getConfigureMock.mockImplementation(() => Promise.resolve(mockCaseConfigure)); + }); + it('push case button posts the push with correct args', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(getConfigureMock).toBeCalled(); + result.current.pushButton.props.children.props.onClick(); + expect(postPushToService).toBeCalledWith({ ...mockConnector, caseId, updateCase }); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + it('Displays message when user does not have premium license', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInLicense: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getLicenseError().title); + }); + }); + it('Displays message when user does not have case enabled in config', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInConfig: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); + }); + }); + it('Displays message when user does not have a connector configured', async () => { + getConfigureMock.mockImplementation(() => + Promise.resolve({ + ...mockCaseConfigure, + connectorId: 'none', + }) + ); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'closed', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx new file mode 100644 index 0000000000000..6109fd05096b9 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; + +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { Case } from '../../../../containers/case/types'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; +import { getConfigureCasesUrl } from '../../../../components/link_to'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../../home/home_navigations'; +import { CaseCallOut } from '../callout'; +import { getLicenseError, getKibanaConfigError } from './helpers'; +import * as i18n from './translations'; + +export interface UsePushToService { + caseId: string; + caseStatus: string; + isNew: boolean; + updateCase: (newCase: Case) => void; + userCanCrud: boolean; +} + +export interface ReturnUsePushToService { + pushButton: JSX.Element; + pushCallouts: JSX.Element | null; +} + +export const usePushToService = ({ + caseId, + caseStatus, + isNew, + updateCase, + userCanCrud, +}: UsePushToService): ReturnUsePushToService => { + const urlSearch = useGetUrlSearch(navTabs.case); + + const { isLoading, postPushToService } = usePostPushToService(); + + const { connectorId, connectorName, loading: loadingCaseConfigure } = useCaseConfigure(); + + const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); + + const handlePushToService = useCallback(() => { + if (connectorId != null) { + postPushToService({ + caseId, + connectorId, + connectorName, + updateCase, + }); + } + }, [caseId, connectorId, connectorName, postPushToService, updateCase]); + + const errorsMsg = useMemo(() => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (connectorId === 'none' && !loadingCaseConfigure && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + <FormattedMessage + defaultMessage="To open and update cases in external systems, you must configure a {link}." + id="xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription" + values={{ + link: ( + <EuiLink href={getConfigureCasesUrl(urlSearch)} target="_blank"> + {i18n.LINK_CONNECTOR_CONFIGURE} + </EuiLink> + ), + }} + /> + ), + }, + ]; + } + if (caseStatus === 'closed') { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, + description: ( + <FormattedMessage + defaultMessage="Closed cases cannot be sent to external systems. Reopen the case if you want to open or update it in an external system." + id="xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription" + /> + ), + }, + ]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; + }, [actionLicense, caseStatus, connectorId, loadingCaseConfigure, loadingLicense, urlSearch]); + + const pushToServiceButton = useMemo( + () => ( + <EuiButton + data-test-subj="push-to-service-now" + fill + iconType="importAction" + onClick={handlePushToService} + disabled={ + isLoading || + loadingLicense || + loadingCaseConfigure || + errorsMsg.length > 0 || + !userCanCrud + } + isLoading={isLoading} + > + {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW} + </EuiButton> + ), + [ + isNew, + handlePushToService, + isLoading, + loadingLicense, + loadingCaseConfigure, + errorsMsg, + userCanCrud, + ] + ); + + const objToReturn = useMemo( + () => ({ + pushButton: + errorsMsg.length > 0 ? ( + <EuiToolTip + position="top" + title={errorsMsg[0].title} + content={<p>{errorsMsg[0].description}</p>} + > + {pushToServiceButton} + </EuiToolTip> + ) : ( + <>{pushToServiceButton}</> + ), + pushCallouts: + errorsMsg.length > 0 ? ( + <CaseCallOut title={i18n.ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={errorsMsg} /> + ) : null, + }), + [errorsMsg, pushToServiceButton] + ); + return objToReturn; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx new file mode 100644 index 0000000000000..96f87c9082945 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { CaseFullExternalService } from '../../../../../../case/common/api'; +import { CaseUserActions } from '../../../../containers/case/types'; +import * as i18n from '../case_view/translations'; + +interface LabelTitle { + action: CaseUserActions; + field: string; + firstIndexPushToService: number; + index: number; +} + +export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: LabelTitle) => { + if (field === 'tags') { + return getTagsLabelTitle(action); + } else if (field === 'title' && action.action === 'update') { + return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + action.newValue + }"`; + } else if (field === 'description' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; + } else if (field === 'status' && action.action === 'update') { + return `${ + action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() + } ${i18n.CASE}`; + } else if (field === 'comment' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; + } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { + return getPushedServiceLabelTitle(action, firstIndexPushToService, index); + } + return ''; +}; + +const getTagsLabelTitle = (action: CaseUserActions) => ( + <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span"> + <EuiFlexItem data-test-subj="ua-tags-label"> + {action.action === 'add' && i18n.ADDED_FIELD} + {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} + </EuiFlexItem> + {action.newValue != null && + action.newValue.split(',').map(tag => ( + <EuiFlexItem grow={false} key={tag}> + <EuiBadge data-test-subj={`ua-tag`} color="default"> + {tag} + </EuiBadge> + </EuiFlexItem> + ))} + </EuiFlexGroup> +); + +const getPushedServiceLabelTitle = ( + action: CaseUserActions, + firstIndexPushToService: number, + index: number +) => { + const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; + return ( + <EuiFlexGroup alignItems="baseline" gutterSize="xs" data-test-subj="pushed-service-label-title"> + <EuiFlexItem data-test-subj="pushed-label"> + {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink data-test-subj="pushed-value" href={pushedVal?.external_url} target="_blank"> + {pushedVal?.connector_name} {pushedVal?.external_title} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/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 0000000000000..f1aea747485e4 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from 'src/core/public'; + +import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; +import { RouteSpyState } from '../../utils/route/types'; +import * as i18n from './translations'; + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + const queryParameters = !isEmpty(search[0]) ? search[0] : null; + + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(queryParameters), + }, + ]; + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_BC_TITLE, + href: getCreateCaseUrl(queryParameters), + }, + ]; + } else if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state?.caseTitle ?? '', + href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/plugins/siem/public/pages/common/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/common/translations.ts rename to x-pack/plugins/siem/public/pages/common/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx index 6212cad7e1845..5428b9932fbde 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; import { TimelineAction } from '../../../../components/timeline/body/actions'; import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; import { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index fd3b9a6f68e82..81b643b7894df 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -10,7 +10,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import ApolloClient from 'apollo-client'; import React from 'react'; -import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; import { TimelineAction, TimelineActionProps } from '../../../../components/timeline/body/actions'; import { defaultColumnHeaderType } from '../../../../components/timeline/body/column_headers/default_headers'; import { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts new file mode 100644 index 0000000000000..a948d2b940b0c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getStringArray, + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + reformatDataProviderWithNewValue, +} from './helpers'; +import { mockEcsData } from '../../../../mock/mock_ecs'; +import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; +import { cloneDeep } from 'lodash/fp'; + +describe('helpers', () => { + let mockEcsDataClone = cloneDeep(mockEcsData); + beforeEach(() => { + mockEcsDataClone = cloneDeep(mockEcsData); + }); + describe('getStringOrStringArray', () => { + test('it should correctly return a string array', () => { + const value = getStringArray('x', { + x: 'The nickname of the developer we all :heart:', + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with a single element', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:'], + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with two elements of strings', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + }); + expect(value).toEqual([ + 'The nickname of the developer we all :heart:', + 'We are all made of stars', + ]); + }); + + test('it should correctly return a string array with deep elements', () => { + const value = getStringArray('x.y.z', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual(['zed']); + }); + + test('it should correctly return a string array with a non-existent value', () => { + const value = getStringArray('non.existent', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual([]); + }); + + test('it should trace an error if the value is not a string', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: 5 }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + 5, + 'when trying to access field:', + 'a', + 'from data object of:', + { a: 5 } + ); + }); + + test('it should trace an error if the value is an array of mixed values', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + ['hi', 5], + 'when trying to access field:', + 'a', + 'from data object of:', + { a: ['hi', 5] } + ); + }); + }); + + describe('replaceTemplateFieldFromQuery', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('it should replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('host.name: apache'); + }); + + test('it should replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + + describe('replaceTemplateFieldFromMatchFilters', () => { + test('given an empty query filter this will return an empty filter', () => { + const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + expect(replacement).toEqual([]); + }); + + test('given a query filter this will return that filter with the placeholder replaced', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Braden' }, + }, + query: { match_phrase: { 'host.name': 'Braden' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'apache' }, + }, + query: { match_phrase: { 'host.name': 'apache' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + + test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + }); + + describe('reformatDataProviderWithNewValue', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts new file mode 100644 index 0000000000000..3fa2da37046b0 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; +import { Filter, esKuery, KueryNode } from '../../../../../../../../src/plugins/data/public'; +import { + DataProvider, + DataProvidersAnd, +} from '../../../../components/timeline/data_providers/data_provider'; +import { Ecs } from '../../../../graphql/types'; + +interface FindValueToChangeInQuery { + field: string; + valueToChange: string; +} + +/** + * Fields that will be replaced with the template strings from a a saved timeline template. + * This is used for the signals detection engine feature when you save a timeline template + * and are the fields you can replace when creating a template. + */ +const templateFields = [ + 'host.name', + 'host.hostname', + 'host.domain', + 'host.id', + 'host.ip', + 'client.ip', + 'destination.ip', + 'server.ip', + 'source.ip', + 'network.community_id', + 'user.name', + 'process.name', +]; + +/** + * This will return an unknown as a string array if it exists from an unknown data type and a string + * that represents the path within the data object the same as lodash's "get". If the value is non-existent + * we will return an empty array. If it is a non string value then this will log a trace to the console + * that it encountered an error and return an empty array. + * @param field string of the field to access + * @param data The unknown data that is typically a ECS value to get the value + * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console + */ +export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { + const value: unknown | undefined = get(field, data); + if (value == null) { + return []; + } else if (typeof value === 'string') { + return [value]; + } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { + return value; + } else { + localConsole.trace( + 'Data type that is not a string or string array detected:', + value, + 'when trying to access field:', + field, + 'from data object of:', + data + ); + return []; + } +}; + +export const findValueToChangeInQuery = ( + kueryNode: KueryNode, + valueToChange: FindValueToChangeInQuery[] = [] +): FindValueToChangeInQuery[] => { + let localValueToChange = valueToChange; + if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { + localValueToChange = [ + ...localValueToChange, + { + field: kueryNode.arguments[0].value, + valueToChange: kueryNode.arguments[1].value, + }, + ]; + } + return kueryNode.arguments.reduce( + (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { + if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { + return [ + ...addValueToChange, + { + field: ast.arguments[0].value, + valueToChange: ast.arguments[1].value, + }, + ]; + } + if (ast.arguments) { + return findValueToChangeInQuery(ast, addValueToChange); + } + return addValueToChange; + }, + localValueToChange + ); +}; + +export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { + if (query.trim() !== '') { + const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); + return valueToChange.reduce((newQuery, vtc) => { + const newValue = getStringArray(vtc.field, ecsData); + if (newValue.length) { + return newQuery.replace(vtc.valueToChange, newValue[0]); + } else { + return newQuery; + } + }, query); + } else { + return ''; + } +}; + +export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => + filters.map(filter => { + if ( + filter.meta.type === 'phrase' && + filter.meta.key != null && + templateFields.includes(filter.meta.key) + ) { + const newValue = getStringArray(filter.meta.key, ecsData); + if (newValue.length) { + filter.meta.params = { query: newValue[0] }; + filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; + } + } + return filter; + }); + +export const reformatDataProviderWithNewValue = <T extends DataProvider | DataProvidersAnd>( + dataProvider: T, + ecsData: Ecs +): T => { + if (templateFields.includes(dataProvider.queryMatch.field)) { + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + if (newValue.length) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); + dataProvider.name = newValue[0]; + dataProvider.queryMatch.value = newValue[0]; + dataProvider.queryMatch.displayField = undefined; + dataProvider.queryMatch.displayValue = undefined; + } + } + return dataProvider; +}; + +export const replaceTemplateFieldFromDataProviders = ( + dataProviders: DataProvider[], + ecsData: Ecs +): DataProvider[] => + dataProviders.map(dataProvider => { + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); + if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { + newDataProvider.and = newDataProvider.and.map(andDataProvider => + reformatDataProviderWithNewValue(andDataProvider, ecsData) + ); + } + return newDataProvider; + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx new file mode 100644 index 0000000000000..5442c8c19b5a7 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -0,0 +1,369 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Filter, esQuery } from '../../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { StatefulEventsViewer } from '../../../../components/events_viewer'; +import { HeaderSection } from '../../../../components/header_section'; +import { combineQueries } from '../../../../components/timeline/helpers'; +import { useKibana } from '../../../../lib/kibana'; +import { inputsSelectors, State, inputsModel } from '../../../../store'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; +import { TimelineModel } from '../../../../store/timeline/model'; +import { timelineDefaults } from '../../../../store/timeline/defaults'; +import { useApolloClient } from '../../../../utils/apollo_context'; + +import { updateSignalStatusAction } from './actions'; +import { + getSignalsActions, + requiredFieldsForActions, + signalsClosedFilters, + signalsDefaultModel, + signalsOpenFilters, +} from './default_config'; +import { + FILTER_CLOSED, + FILTER_OPEN, + SignalFilterOption, + SignalsTableFilterGroup, +} from './signals_filter_group'; +import { SignalsUtilityBar } from './signals_utility_bar'; +import * as i18n from './translations'; +import { + CreateTimelineProps, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateSignalsStatusCallback, + UpdateSignalsStatusProps, +} from './types'; +import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; + +export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; + +interface OwnProps { + canUserCRUD: boolean; + defaultFilters?: Filter[]; + hasIndexWrite: boolean; + from: number; + loading: boolean; + signalsIndex: string; + to: number; +} + +type SignalsTableComponentProps = OwnProps & PropsFromRedux; + +export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ + canUserCRUD, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + defaultFilters, + from, + globalFilters, + globalQuery, + hasIndexWrite, + isSelectAllChecked, + loading, + loadingEventIds, + selectedEventIds, + setEventsDeleted, + setEventsLoading, + signalsIndex, + to, + updateTimeline, + updateTimelineIsLoading, +}) => { + const [selectAll, setSelectAll] = useState(false); + const apolloClient = useApolloClient(); + + const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); + const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + signalsIndex !== '' ? [signalsIndex] : [] + ); + const kibana = useKibana(); + + const getGlobalQuery = useCallback(() => { + if (browserFields != null && indexPatterns != null) { + return combineQueries({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders: [], + indexPattern: indexPatterns, + browserFields, + filters: isEmpty(defaultFilters) + ? globalFilters + : [...(defaultFilters ?? []), ...globalFilters], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + start: from, + end: to, + isEventViewer: true, + }); + } + return null; + }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); + + // Callback for creating a new timeline -- utilized by row/batch actions + const createTimelineCallback = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, + id: 'timeline-1', + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [updateTimeline, updateTimelineIsLoading] + ); + + const setEventsLoadingCallback = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); + }, + [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] + ); + + const setEventsDeletedCallback = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); + }, + [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] + ); + + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar + useEffect(() => { + if (!isSelectAllChecked) { + setShowClearSelectionAction(false); + } else { + setSelectAll(false); + } + }, [isSelectAllChecked]); + + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: SignalFilterOption) => { + clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + + // Callback for clearing entire selection from utility bar + const clearSelectionCallback = useCallback(() => { + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setSelectAll(false); + setShowClearSelectionAction(false); + }, [clearSelected, setSelectAll, setShowClearSelectionAction]); + + // Callback for selecting all events on all pages from utility bar + // Dispatches to stateful_body's selectAll via TimelineTypeContext props + // as scope of response data required to actually set selectedEvents + const selectAllCallback = useCallback(() => { + setSelectAll(true); + setShowClearSelectionAction(true); + }, [setSelectAll, setShowClearSelectionAction]); + + const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( + async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { + await updateSignalStatusAction({ + query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, + signalIds: Object.keys(selectedEventIds), + status, + setEventsDeleted: setEventsDeletedCallback, + setEventsLoading: setEventsLoadingCallback, + }); + refetchQuery(); + }, + [ + getGlobalQuery, + selectedEventIds, + setEventsDeletedCallback, + setEventsLoadingCallback, + showClearSelectionAction, + ] + ); + + // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component + const utilityBarCallback = useCallback( + (refetchQuery: inputsModel.Refetch, totalCount: number) => { + return ( + <SignalsUtilityBar + canUserCRUD={canUserCRUD} + areEventsLoading={loadingEventIds.length > 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + isFilteredToOpen={filterGroup === FILTER_OPEN} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} + /> + ); + }, + [ + canUserCRUD, + hasIndexWrite, + clearSelectionCallback, + filterGroup, + loadingEventIds.length, + selectAllCallback, + selectedEventIds, + showClearSelectionAction, + updateSignalsStatusCallback, + ] + ); + + // Send to Timeline / Update Signal Status Actions for each table row + const additionalActions = useMemo( + () => + getSignalsActions({ + apolloClient, + canUserCRUD, + hasIndexWrite, + createTimeline: createTimelineCallback, + setEventsLoading: setEventsLoadingCallback, + setEventsDeleted: setEventsDeletedCallback, + status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, + updateTimelineIsLoading, + }), + [ + apolloClient, + canUserCRUD, + createTimelineCallback, + hasIndexWrite, + filterGroup, + setEventsLoadingCallback, + setEventsDeletedCallback, + updateTimelineIsLoading, + ] + ); + + const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); + const defaultFiltersMemo = useMemo(() => { + if (isEmpty(defaultFilters)) { + return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; + } else if (defaultFilters != null && !isEmpty(defaultFilters)) { + return [ + ...defaultFilters, + ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), + ]; + } + }, [defaultFilters, filterGroup]); + + const timelineTypeContext = useMemo( + () => ({ + documentType: i18n.SIGNALS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_SIGNALS, + loadingText: i18n.LOADING_SIGNALS, + queryFields: requiredFieldsForActions, + timelineActions: additionalActions, + title: i18n.SIGNALS_TABLE_TITLE, + selectAll: canUserCRUD ? selectAll : false, + }), + [additionalActions, canUserCRUD, selectAll] + ); + + const headerFilterGroup = useMemo( + () => <SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />, + [onFilterGroupChangedCallback] + ); + + if (loading || isEmpty(signalsIndex)) { + return ( + <EuiPanel> + <HeaderSection title={i18n.SIGNALS_TABLE_TITLE} /> + <EuiLoadingContent data-test-subj="loading-signals-panel" /> + </EuiPanel> + ); + } + + return ( + <StatefulEventsViewer + defaultIndices={defaultIndices} + pageFilters={defaultFiltersMemo} + defaultModel={signalsDefaultModel} + end={to} + headerFilterGroup={headerFilterGroup} + id={SIGNALS_PAGE_TIMELINE_ID} + start={from} + timelineTypeContext={timelineTypeContext} + utilityBar={utilityBarCallback} + /> + ); +}; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalInputs = inputsSelectors.globalSelector(); + const mapStateToProps = (state: State) => { + const timeline: TimelineModel = + getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; + const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; + + const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + return { + globalQuery: query, + globalFilters: filters, + deletedEventIds, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx new file mode 100644 index 0000000000000..b9268716f85f0 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/utility_bar'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../../../../lib/kibana'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { UpdateSignalsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; + +interface SignalsUtilityBarProps { + canUserCRUD: boolean; + hasIndexWrite: boolean; + areEventsLoading: boolean; + clearSelection: () => void; + isFilteredToOpen: boolean; + selectAll: () => void; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showClearSelection: boolean; + totalCount: number; + updateSignalsStatus: UpdateSignalsStatus; +} + +const SignalsUtilityBarComponent: React.FC<SignalsUtilityBarProps> = ({ + canUserCRUD, + hasIndexWrite, + areEventsLoading, + clearSelection, + totalCount, + selectedEventIds, + isFilteredToOpen, + selectAll, + showClearSelection, + updateSignalsStatus, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + + const handleUpdateStatus = useCallback(async () => { + await updateSignalsStatus({ + signalIds: Object.keys(selectedEventIds), + status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, + }); + }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); + + const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); + const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( + defaultNumberFormat + ); + + return ( + <> + <UtilityBar> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText dataTestSubj="showingSignals"> + {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + </UtilityBarText> + </UtilityBarGroup> + + <UtilityBarGroup> + {canUserCRUD && hasIndexWrite && ( + <> + <UtilityBarText dataTestSubj="selectedSignals"> + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + </UtilityBarText> + + <UtilityBarAction + dataTestSubj="openCloseSignal" + disabled={areEventsLoading || isEmpty(selectedEventIds)} + iconType={isFilteredToOpen ? 'securitySignalResolved' : 'securitySignalDetected'} + onClick={handleUpdateStatus} + > + {isFilteredToOpen + ? i18n.BATCH_ACTION_CLOSE_SELECTED + : i18n.BATCH_ACTION_OPEN_SELECTED} + </UtilityBarAction> + + <UtilityBarAction + iconType={showClearSelection ? 'cross' : 'pagesSelect'} + onClick={() => { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + </UtilityBarAction> + </> + )} + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> + </> + ); +}; + +export const SignalsUtilityBar = React.memo( + SignalsUtilityBarComponent, + (prevProps, nextProps) => + prevProps.areEventsLoading === nextProps.areEventsLoading && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.totalCount === nextProps.totalCount && + prevProps.showClearSelection === nextProps.showClearSelection +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx new file mode 100644 index 0000000000000..24b12cae62d85 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { showAllOthersBucket } from '../../../../../common/constants'; +import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; +import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; +import * as i18n from './translations'; + +export const formatSignalsData = ( + signalsData: SignalSearchResponse<{}, SignalsAggregation> | null +) => { + const groupBuckets: SignalsGroupBucket[] = + signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; + return groupBuckets.reduce<HistogramData[]>((acc, { key: group, signals }) => { + const signalsBucket: SignalsBucket[] = signals.buckets ?? []; + + return [ + ...acc, + ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ + x: key, + y: doc_count, + g: group, + })), + ]; + }, []); +}; + +export const getSignalsHistogramQuery = ( + stackByField: string, + from: number, + to: number, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> +) => { + const missing = showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + + return { + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${Math.floor((to - from) / 32)}ms`, + min_doc_count: 0, + extended_bounds: { + min: from, + max: to, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + }; +}; + +/** + * Returns `true` when the signals histogram initial loading spinner should be shown + * + * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed + * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) + */ +export const showInitialLoadingSpinner = ({ + isInitialLoading, + isLoadingSignals, +}: { + isInitialLoading: boolean; + isLoadingSignals: boolean; +}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx new file mode 100644 index 0000000000000..cf6730ea3ec3d --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Position } from '@elastic/charts'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; +import { HeaderSection } from '../../../../components/header_section'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; +import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; +import { InspectButtonContainer } from '../../../../components/inspect'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; +import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; +import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; +import { navTabs } from '../../../home/home_navigations'; +import { signalsHistogramOptions } from './config'; +import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { SignalsHistogram } from './signals_histogram'; +import * as i18n from './translations'; +import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; + +const DEFAULT_PANEL_HEIGHT = 300; + +const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` + display: flex; + flex-direction: column; + ${({ height }) => (height != null ? `height: ${height}px;` : '')} + position: relative; +`; + +const defaultTotalSignalsObj: SignalsTotal = { + value: 0, + relation: 'eq', +}; + +export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; + +const ViewSignalsFlexItem = styled(EuiFlexItem)` + margin-left: 24px; +`; + +interface SignalsHistogramPanelProps { + chartHeight?: number; + defaultStackByOption?: SignalsHistogramOption; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + legendPosition?: Position; + panelHeight?: number; + signalIndexName: string | null; + setQuery: (params: RegisterQuery) => void; + showLinkToSignals?: boolean; + showTotalSignalsCount?: boolean; + stackByOptions?: SignalsHistogramOption[]; + title?: string; + to: number; + updateDateRange: (min: number, max: number) => void; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const NO_LEGEND_DATA: LegendItem[] = []; + +export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( + ({ + chartHeight, + defaultStackByOption = signalsHistogramOptions[0], + deleteQuery, + filters, + headerChildren, + onlyField, + query, + from, + legendPosition = 'right', + panelHeight = DEFAULT_PANEL_HEIGHT, + setQuery, + signalIndexName, + showLinkToSignals = false, + showTotalSignalsCount = false, + stackByOptions, + to, + title = i18n.HISTOGRAM_HEADER, + updateDateRange, + }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj); + const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>( + onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) + ); + const { + loading: isLoadingSignals, + data: signalsData, + setQuery: setSignalsQuery, + response, + request, + refetch, + } = useQuerySignals<{}, SignalsAggregation>( + getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), + signalIndexName + ); + const kibana = useKibana(); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const totalSignals = useMemo( + () => + i18n.SHOWING_SIGNALS( + numeral(totalSignalsObj.value).format(defaultNumberFormat), + totalSignalsObj.value, + totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' + ), + [totalSignalsObj] + ); + + const setSelectedOptionCallback = useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { + setSelectedStackByOption( + stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + ); + }, []); + + const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + + const legendItems: LegendItem[] = useMemo( + () => + signalsData?.aggregations?.signalsByGrouping?.buckets != null + ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + ), + field: selectedStackByOption.value, + value: bucket.key, + })) + : NO_LEGEND_DATA, + [signalsData, selectedStackByOption.value] + ); + + useEffect(() => { + let canceled = false; + + if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { + setIsInitialLoading(false); + } + + return () => { + canceled = true; // prevent long running data fetches from updating state after unmounting + }; + }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, []); + + useEffect(() => { + if (refetch != null && setQuery != null) { + setQuery({ + id: uniqueQueryId, + inspect: { + dsl: [request], + response: [response], + }, + loading: isLoadingSignals, + refetch, + }); + } + }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); + + useEffect(() => { + setTotalSignalsObj( + signalsData?.hits.total ?? { + value: 0, + relation: 'eq', + } + ); + }, [signalsData]); + + useEffect(() => { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter(f => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); + + setSignalsQuery( + getSignalsHistogramQuery( + selectedStackByOption.value, + from, + to, + !isEmpty(converted) ? [converted] : [] + ) + ); + }, [selectedStackByOption.value, from, to, query, filters]); + + const linkButton = useMemo(() => { + if (showLinkToSignals) { + return ( + <ViewSignalsFlexItem grow={false}> + <EuiButton href={getDetectionEngineUrl(urlSearch)}>{i18n.VIEW_SIGNALS}</EuiButton> + </ViewSignalsFlexItem> + ); + } + }, [showLinkToSignals, urlSearch]); + + const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ + onlyField, + title, + ]); + + return ( + <InspectButtonContainer data-test-subj="signals-histogram-panel" show={!isInitialLoading}> + <StyledEuiPanel height={panelHeight}> + <HeaderSection + id={uniqueQueryId} + title={titleText} + titleSize={onlyField == null ? 'm' : 's'} + subtitle={!isInitialLoading && showTotalSignalsCount && totalSignals} + > + <EuiFlexGroup alignItems="center" gutterSize="none"> + <EuiFlexItem grow={false}> + {stackByOptions && ( + <EuiSelect + onChange={setSelectedOptionCallback} + options={stackByOptions} + prepend={i18n.STACK_BY_LABEL} + value={selectedStackByOption.value} + /> + )} + {headerChildren != null && headerChildren} + </EuiFlexItem> + {linkButton} + </EuiFlexGroup> + </HeaderSection> + + {isInitialLoading ? ( + <MatrixLoader /> + ) : ( + <SignalsHistogram + chartHeight={chartHeight} + data={formattedSignalsData} + from={from} + legendItems={legendItems} + legendPosition={legendPosition} + loading={isLoadingSignals} + to={to} + updateDateRange={updateDateRange} + /> + )} + </StyledEuiPanel> + </InspectButtonContainer> + ); + } +); + +SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts rename to x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts new file mode 100644 index 0000000000000..66964fae70f94 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; +import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; +import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { FieldValueQueryBar } from '../../components/query_bar'; + +export const mockQueryBar: FieldValueQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +export const mockRule = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: [], + filters: [], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Home Grown!', + query: '', + references: [], + saved_id: "Garrett's IP", + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Untitled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'saved_query', + threat: [], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockRuleWithEverything = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: ['test'], + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Query with rule-id', + query: 'user.name: root or user.name: admin', + references: ['www.test.co'], + saved_id: 'test123', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: ['tag1', 'tag2'], + to: 'now', + type: 'saved_query', + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ + isNew, + name: 'Query with rule-id', + description: '24/7', + severity: 'low', + riskScore: 21, + references: ['www.test.co'], + falsePositives: ['test'], + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', +}); + +export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ + isNew, + actions: [], + kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + enabled, + throttle: 'no_actions', +}); + +export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ + isNew, + ruleType: 'query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['filebeat-'], + queryBar: mockQueryBar, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, +}); + +export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ + isNew, + interval: '5m', + from: '6m', + to: 'now', +}); + +export const mockRuleError = (id: string): RuleError => ({ + rule_id: id, + error: { status_code: 404, message: `id: "${id}" not found` }, +}); + +export const mockRules: Rule[] = [ + mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), + mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx diff --git a/x-pack/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 97c89f91c12bd..d383b5cd464ce 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 0000000000000..186aeae42246d --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -0,0 +1,415 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { esFilters, FilterManager } from '../../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: 'test query', + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: '', + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when "query.query" exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} </>); + }); + + test('returns expected array of ListItems when "savedId" exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: '', + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} </>); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .first() + .text() + ).toEqual('tag1'); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .at(1) + .text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(<SeverityBadge value="Test description value" />); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .first() + .text() + ).toEqual('www.test.com'); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .at(1) + .text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and note content', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); + }); + + describe('buildRuleTypeDescription', () => { + it('returns the label for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.description).toEqual('Machine Learning'); + }); + + it('returns the label for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.description).toEqual('Query'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx new file mode 100644 index 0000000000000..1ac371a3f6829 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + +import * as i18n from './translations'; +import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; +import { SeverityBadge } from '../severity_badge'; +import ListTreeIcon from './assets/list_tree_icon.svg'; +import { assertUnreachable } from '../../../../../lib/helpers'; + +const NoteDescriptionContainer = styled(EuiFlexItem)` + height: 105px; + overflow-y: hidden; +`; + +export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); + +const EuiBadgeWrap = (styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +` as unknown) as typeof EuiBadge; + +export const buildQueryBarDescription = ({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, +}: BuildQueryBarDescription): ListItems[] => { + let items: ListItems[] = []; + if (!isEmpty(filters)) { + filterManager.setFilters(filters); + items = [ + ...items, + { + title: <>{i18n.FILTERS_LABEL} </>, + description: ( + <EuiFlexGroup wrap responsive={false} gutterSize="xs"> + {filterManager.getFilters().map((filter, index) => ( + <EuiFlexItem grow={false} key={`${field}-filter-${index}`}> + <EuiBadgeWrap color="hollow"> + {indexPatterns != null ? ( + <esFilters.FilterLabel + filter={filter} + valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])} + /> + ) : ( + <EuiLoadingSpinner size="m" /> + )} + </EuiBadgeWrap> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ), + }, + ]; + } + if (!isEmpty(query)) { + items = [ + ...items, + { + title: <>{i18n.QUERY_LABEL} </>, + description: <>{query} </>, + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{i18n.SAVED_ID_LABEL} </>, + description: <>{savedId} </>, + }, + ]; + } + return items; +}; + +const ThreatEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + +const TechniqueLinkItem = styled(EuiButtonEmpty)` + .euiIcon { + width: 8px; + height: 8px; + } +`; + +export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { + if (threat.length > 0) { + return [ + { + title: label, + description: ( + <ThreatEuiFlexGroup direction="column"> + {threat.map((singleThreat, index) => { + const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); + return ( + <EuiFlexItem key={`${singleThreat.tactic.name}-${index}`}> + <EuiLink + data-test-subj="threatTacticLink" + href={singleThreat.tactic.reference} + target="_blank" + > + {tactic != null ? tactic.text : ''} + </EuiLink> + <EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column"> + {singleThreat.technique.map(technique => { + const myTechnique = techniquesOptions.find(t => t.id === technique.id); + return ( + <EuiFlexItem> + <TechniqueLinkItem + data-test-subj="threatTechniqueLink" + href={technique.reference} + target="_blank" + iconType={ListTreeIcon} + size="xs" + flush="left" + > + {myTechnique != null ? myTechnique.label : ''} + </TechniqueLinkItem> + </EuiFlexItem> + ); + })} + </EuiFlexGroup> + </EuiFlexItem> + ); + })} + <EuiSpacer /> + </ThreatEuiFlexGroup> + ), + }, + ]; + } + return []; +}; + +export const buildUnorderedListArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiText size="s"> + <ul> + {values.map(val => + isEmpty(val) ? null : ( + <li data-test-subj="unorderedListArrayDescriptionItem" key={`${field}-${val}`}> + {val} + </li> + ) + )} + </ul> + </EuiText> + ), + }, + ]; + } + return []; +}; + +export const buildStringArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiFlexGroup responsive={false} gutterSize="xs" wrap> + {values.map((val: string) => + isEmpty(val) ? null : ( + <EuiFlexItem grow={false} key={`${field}-${val}`}> + <EuiBadgeWrap data-test-subj="stringArrayDescriptionBadgeItem" color="hollow"> + {val} + </EuiBadgeWrap> + </EuiFlexItem> + ) + )} + </EuiFlexGroup> + ), + }, + ]; + } + return []; +}; + +export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ + { + title: label, + description: <SeverityBadge value={value} />, + }, +]; + +export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiText size="s"> + <ul> + {values + .filter(v => !isEmpty(v)) + .map((val, index) => ( + <li data-test-subj="urlsDescriptionReferenceLinkItem" key={`${index}-${val}`}> + <EuiLink href={val} external target="_blank"> + {val} + </EuiLink> + </li> + ))} + </ul> + </EuiText> + ), + }, + ]; + } + return []; +}; + +export const buildNoteDescription = (label: string, note: string): ListItems[] => { + if (note.trim() !== '') { + return [ + { + title: label, + description: ( + <NoteDescriptionContainer> + <div data-test-subj="noteDescriptionItem" className="eui-yScrollWithShadows"> + {note} + </div> + </NoteDescriptionContainer> + ), + }, + ]; + } + return []; +}; + +export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { + switch (ruleType) { + case 'machine_learning': { + return [ + { + title: label, + description: i18n.ML_TYPE_DESCRIPTION, + }, + ]; + } + case 'query': + case 'saved_query': { + return [ + { + title: label, + description: i18n.QUERY_TYPE_DESCRIPTION, + }, + ]; + } + default: + return assertUnreachable(ruleType); + } +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx new file mode 100644 index 0000000000000..fdfcfd0fd85fe --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -0,0 +1,474 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from './'; + +import { + esFilters, + Filter, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { mockAboutStepRule, mockDefineStepRule } from '../../all/__mocks__/mock'; +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; +import { AboutStepRule } from '../../types'; + +jest.mock('../../../../../lib/kibana'); + +describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; + + beforeEach(() => { + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is "multi"', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is "single"', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is "singleSplit', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper + .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') + .at(0) + .prop('type') + ).toEqual('column'); + }); + }); + + describe('addFilterStateIfNotThere', () => { + test('it does not change the state if it is global', () => { + const filters: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + + test('it adds the state if it does not exist as local', () => { + const filters: Filter[] = [ + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); + + expect(result.length).toEqual(9); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when "value" is a non-existant property in "field"', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} </>); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of "none"', () => { + const mockStep = { + ...mockAboutStep, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(21); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const mockDefineStep = mockDefineStepRule(); + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockDefineStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockStep = { + ...mockDefineStepRule(), + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default "note" description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation guide', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation guide'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx new file mode 100644 index 0000000000000..108f213811412 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; +import React, { memo, useState } from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { + IIndexPattern, + Filter, + esFilters, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import { useKibana } from '../../../../../lib/kibana'; +import { IMitreEnterpriseAttack } from '../../types'; +import { FieldValueTimeline } from '../pick_timeline'; +import { FormSchema } from '../../../../../shared_imports'; +import { ListItems } from './types'; +import { + buildQueryBarDescription, + buildSeverityDescription, + buildStringArrayDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { buildMlJobDescription } from './ml_job_description'; + +const DescriptionListContainer = styled(EuiDescriptionList)` + &.euiDescriptionList--column .euiDescriptionList__title { + width: 30%; + } + &.euiDescriptionList--column .euiDescriptionList__description { + width: 70%; + } +`; + +interface StepRuleDescriptionProps { + columns?: 'multi' | 'single' | 'singleSplit'; + data: unknown; + indexPatterns?: IIndexPattern; + schema: FormSchema; +} + +export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({ + data, + columns = 'multi', + indexPatterns, + schema, +}) => { + const kibana = useKibana(); + const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); + const [, siemJobs] = useSiemJobs(true); + + const keys = Object.keys(schema); + const listItems = keys.reduce((acc: ListItems[], key: string) => { + if (key === 'machineLearningJobId') { + return [ + ...acc, + buildMlJobDescription( + get(key, data) as string, + (get(key, schema) as { label: string }).label, + siemJobs + ), + ]; + } + return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; + }, []); + + if (columns === 'multi') { + return ( + <EuiFlexGroup> + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( + <EuiFlexItem + data-test-subj="listItemColumnStepRuleDescription" + key={`description-step-rule-${index}`} + > + <EuiDescriptionList listItems={chunkListItems} /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); + } + + return ( + <EuiFlexGroup> + <EuiFlexItem data-test-subj="listItemColumnStepRuleDescription"> + {columns === 'single' ? ( + <EuiDescriptionList listItems={listItems} /> + ) : ( + <DescriptionListContainer + data-test-subj="singleSplitStepRuleDescriptionList" + type="column" + listItems={listItems} + /> + )} + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const StepRuleDescription = memo(StepRuleDescriptionComponent); + +export const buildListItems = ( + data: unknown, + schema: FormSchema, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => + Object.keys(schema).reduce<ListItems[]>( + (acc, field) => [ + ...acc, + ...getDescriptionItem( + field, + get([field, 'label'], schema), + data, + filterManager, + indexPatterns + ), + ], + [] + ); + +export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { + return filters.map(filter => { + if (filter.$state == null) { + return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; + } else { + return filter; + } + }); +}; + +export const getDescriptionItem = ( + field: string, + label: string, + data: unknown, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => { + if (field === 'queryBar') { + const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); + const query = get('queryBar.query.query', data); + const savedId = get('queryBar.saved_id', data); + return buildQueryBarDescription({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, + }); + } else if (field === 'threat') { + const threat: IMitreEnterpriseAttack[] = get(field, data).filter( + (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' + ); + return buildThreatDescription({ label, threat }); + } else if (field === 'references') { + const urls: string[] = get(field, data); + return buildUrlsDescription(label, urls); + } else if (field === 'falsePositives') { + const values: string[] = get(field, data); + return buildUnorderedListArrayDescription(label, field, values); + } else if (Array.isArray(get(field, data))) { + const values: string[] = get(field, data); + return buildStringArrayDescription(label, field, values); + } else if (field === 'severity') { + const val: string = get(field, data); + return buildSeverityDescription(label, val); + } else if (field === 'timeline') { + const timeline = get(field, data) as FieldValueTimeline; + return [ + { + title: label, + description: timeline.title ?? DEFAULT_TIMELINE_TITLE, + }, + ]; + } else if (field === 'note') { + const val: string = get(field, data); + return buildNoteDescription(label, val); + } else if (field === 'ruleType') { + const ruleType: RuleType = get(field, data); + return buildRuleTypeDescription(label, ruleType); + } + + const description: string = get(field, data); + if (isNumber(description) || !isEmpty(description)) { + return [ + { + title: label, + description, + }, + ]; + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx index 5e8681a90d428..79993c37e549c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx @@ -8,7 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; -import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; import { useKibana } from '../../../../../lib/kibana'; import { SiemJob } from '../../../../../components/ml_popover/types'; import { ListItems } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts new file mode 100644 index 0000000000000..564a3c5dc2c01 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ReactNode } from 'react'; + +import { + IIndexPattern, + Filter, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { IMitreEnterpriseAttack } from '../../types'; + +export interface ListItems { + title: NonNullable<ReactNode>; + description: NonNullable<ReactNode>; +} + +export interface BuildQueryBarDescription { + field: string; + filters: Filter[]; + filterManager: FilterManager; + query: string; + savedId: string; + indexPatterns?: IIndexPattern; +} + +export interface BuildThreatDescription { + label: string; + threat: IMitreEnterpriseAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx new file mode 100644 index 0000000000000..4fb9faaea711c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; + +import styled from 'styled-components'; +import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { useKibana } from '../../../../../lib/kibana'; +import { + ML_JOB_SELECT_PLACEHOLDER_TEXT, + ENABLE_ML_JOB_WARNING, +} from '../step_define_rule/translations'; + +const HelpTextWarningContainer = styled.div` + margin-top: 10px; +`; + +const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 5px; +`; + +const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ + href, + showEnableWarning = false, +}) => ( + <> + <FormattedMessage + id="xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText" + defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of “siem” to those jobs in the {machineLearning} application to make them appear here." + values={{ + machineLearning: ( + <EuiLink href={href} target="_blank"> + <FormattedMessage + id="xpack.siem.components.mlJobSelect.machineLearningLink" + defaultMessage="Machine Learning" + /> + </EuiLink> + ), + }} + /> + {showEnableWarning && ( + <HelpTextWarningContainer> + <EuiText size="xs" color="warning"> + <EuiIcon type="alert" /> + <span>{ENABLE_ML_JOB_WARNING}</span> + </EuiText> + </HelpTextWarningContainer> + )} + </> +); + +const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( + <> + <strong>{title}</strong> + <EuiText size="xs" color="subdued"> + <p>{description}</p> + </EuiText> + </> +); + +interface MlJobSelectProps { + describedByIds: string[]; + field: FieldHook; +} + +export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const mlUrl = useKibana().services.application.getUrlForApp('ml'); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + const placeholderOption = { + value: 'placeholder', + inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + disabled: true, + }; + + const jobOptions = siemJobs.map(job => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: <JobDisplay title={job.id} description={job.description} />, + })); + + const options = [placeholderOption, ...jobOptions]; + + const isJobRunning = useMemo(() => { + // If the selected job is not found in the list, it means the placeholder is selected + // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' + const job = siemJobs.find(j => j.id === jobId); + return job == null || isJobStarted(job.jobState, job.datafeedState); + }, [siemJobs, jobId]); + + return ( + <MlJobSelectEuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label={field.label} + helpText={<HelpText href={mlUrl} showEnableWarning={!isJobRunning} />} + isInvalid={isInvalid} + error={errorMessage} + data-test-subj="mlJobSelect" + describedByIds={describedByIds} + > + <EuiFlexGroup> + <EuiFlexItem> + <EuiSuperSelect + hasDividers + isLoading={isLoading} + onChange={handleJobChange} + options={options} + valueOfSelected={jobId || 'placeholder'} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiFlexItem> + </MlJobSelectEuiFlexGroup> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx new file mode 100644 index 0000000000000..b92d98a4afb13 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -0,0 +1,285 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + Query, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../../../containers/source'; +import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; +import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; +import { QueryBar } from '../../../../../components/query_bar'; +import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; +import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; +import { useKibana } from '../../../../../lib/kibana'; +import { TimelineModel } from '../../../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import * as i18n from './translations'; + +export interface FieldValueQueryBar { + filters: Filter[]; + query: Query; + saved_id?: string; +} +interface QueryBarDefineRuleProps { + browserFields: BrowserFields; + dataTestSubj: string; + field: FieldHook; + idAria: string; + isLoading: boolean; + indexPattern: IIndexPattern; + onCloseTimelineSearch: () => void; + openTimelineSearch: boolean; + resizeParentContainer?: (height: number) => void; +} + +const StyledEuiFormRow = styled(EuiFormRow)` + .kbnTypeahead__items { + max-height: 45vh !important; + } + .globalQueryBar { + padding: 4px 0px 0px 0px; + .kbnQueryBar { + & > div:first-child { + margin: 0px 0px 0px 4px; + } + } + } +`; + +// TODO need to add disabled in the SearchBar + +export const QueryBarDefineRule = ({ + browserFields, + dataTestSubj, + field, + idAria, + indexPattern, + isLoading = false, + onCloseTimelineSearch, + openTimelineSearch = false, + resizeParentContainer, +}: QueryBarDefineRuleProps) => { + const [originalHeight, setOriginalHeight] = useState(-1); + const [loadingTimeline, setLoadingTimeline] = useState(false); + const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); + const [queryDraft, setQueryDraft] = useState<Query>({ query: '', language: 'kuery' }); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const kibana = useKibana(); + const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters([]); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const newFilters = filterManager.getFilters(); + const { filters } = field.value as FieldValueQueryBar; + + if (!deepEqual(filters, newFilters)) { + field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + } + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, [field.value]); + + useEffect(() => { + let isSubscribed = true; + async function updateFilterQueryFromValue() { + const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; + if (!deepEqual(query, queryDraft)) { + setQueryDraft(query); + } + if (!deepEqual(filters, filterManager.getFilters())) { + filterManager.setFilters(filters); + } + if ( + (savedId != null && savedQuery != null && savedId !== savedQuery.id) || + (savedId != null && savedQuery == null) + ) { + try { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery(mySavedQuery); + } + } catch { + setSavedQuery(null); + } + } else if (savedId == null && savedQuery != null) { + setSavedQuery(null); + } + } + updateFilterQueryFromValue(); + return () => { + isSubscribed = false; + }; + }, [field.value]); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + const { saved_id: savedId } = field.value as FieldValueQueryBar; + if (newSavedQuery.id !== savedId) { + setSavedQuery(newSavedQuery); + field.setValue({ + filters: newSavedQuery.attributes.filters, + query: newSavedQuery.attributes.query, + saved_id: newSavedQuery.id, + }); + } + } + }, + [field.value] + ); + + const onCloseTimelineModal = useCallback(() => { + setLoadingTimeline(true); + onCloseTimelineSearch(); + }, [onCloseTimelineSearch]); + + const onOpenTimeline = useCallback( + (timeline: TimelineModel) => { + setLoadingTimeline(false); + const newQuery = { + query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', + language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', + }; + const dataProvidersDsl = + timeline.dataProviders != null && timeline.dataProviders.length > 0 + ? convertKueryToElasticSearchQuery( + buildGlobalQuery(timeline.dataProviders, browserFields), + indexPattern + ) + : ''; + const newFilters = timeline.filters ?? []; + field.setValue({ + filters: + dataProvidersDsl !== '' + ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] + : newFilters, + query: newQuery, + saved_id: '', + }); + }, + [browserFields, field, indexPattern] + ); + + const onMutation = (event: unknown, observer: unknown) => { + if (resizeParentContainer != null) { + const suggestionContainer = document.getElementById('kbnTypeahead__items'); + if (suggestionContainer != null) { + const box = suggestionContainer.getBoundingClientRect(); + const accordionContainer = document.getElementById('define-rule'); + if (accordionContainer != null) { + const accordionBox = accordionContainer.getBoundingClientRect(); + if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { + resizeParentContainer(originalHeight + box.height - 100); + } + if (originalHeight === -1) { + setOriginalHeight(accordionBox.height); + } + } + } else { + resizeParentContainer(-1); + } + } + }; + + const actionTimelineToHide = useMemo<ActionTimelineToShow[]>(() => ['duplicate'], []); + + return ( + <> + <StyledEuiFormRow + label={field.label} + labelAppend={field.labelAppend} + helpText={field.helpText} + error={errorMessage} + isInvalid={isInvalid} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + <EuiMutationObserver + observerOptions={{ subtree: true, attributes: true, childList: true }} + onMutation={onMutation} + > + {mutationRef => ( + <div ref={mutationRef}> + <QueryBar + indexPattern={indexPattern} + isLoading={isLoading || loadingTimeline} + isRefreshPaused={false} + filterQuery={queryDraft} + filterManager={filterManager} + filters={filterManager.getFilters() || []} + onChangedQuery={onChangedQuery} + onSubmitQuery={onSubmitQuery} + savedQuery={savedQuery} + onSavedQuery={onSavedQuery} + hideSavedQuery={false} + /> + </div> + )} + </EuiMutationObserver> + </StyledEuiFormRow> + {openTimelineSearch ? ( + <OpenTimelineModal + hideActions={actionTimelineToHide} + modalTitle={i18n.IMPORT_TIMELINE_MODAL} + onClose={onCloseTimelineModal} + onOpen={onOpenTimeline} + /> + ) : null} + </> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx new file mode 100644 index 0000000000000..b743888b67815 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import deepMerge from 'deepmerge'; + +import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loadActionTypes } from '../../../../../../../triggers_actions_ui/public/application/lib/action_connector_api'; +import { SelectField } from '../../../../../shared_imports'; +import { ActionForm, ActionType } from '../../../../../../../triggers_actions_ui/public'; +import { AlertAction } from '../../../../../../../alerting/common'; +import { useKibana } from '../../../../../lib/kibana'; + +type ThrottleSelectField = typeof SelectField; + +const DEFAULT_ACTION_GROUP_ID = 'default'; +const DEFAULT_ACTION_MESSAGE = + 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; + +export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { + const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); + const { + http, + triggers_actions_ui: { actionTypeRegistry }, + notifications, + } = useKibana().services; + + const setActionIdByIndex = useCallback( + (id: string, index: number) => { + const updatedActions = [...(field.value as Array<Partial<AlertAction>>)]; + updatedActions[index] = deepMerge(updatedActions[index], { id }); + field.setValue(updatedActions); + }, + [field] + ); + + const setAlertProperty = useCallback( + (updatedActions: AlertAction[]) => field.setValue(updatedActions), + [field] + ); + + const setActionParamsProperty = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (key: string, value: any, index: number) => { + const updatedActions = [...(field.value as AlertAction[])]; + updatedActions[index].params[key] = value; + field.setValue(updatedActions); + }, + [field] + ); + + useEffect(() => { + (async function() { + const actionTypes = await loadActionTypes({ http }); + const supportedTypes = actionTypes.filter(actionType => + NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) + ); + setSupportedActionTypes(supportedTypes); + })(); + }, []); + + if (!supportedActionTypes) return <></>; + + return ( + <ActionForm + actions={field.value as AlertAction[]} + messageVariables={messageVariables} + defaultActionGroupId={DEFAULT_ACTION_GROUP_ID} + setActionIdByIndex={setActionIdByIndex} + setAlertProperty={setAlertProperty} + setActionParamsProperty={setActionParamsProperty} + http={http} + actionTypeRegistry={actionTypeRegistry} + actionTypes={supportedActionTypes} + defaultActionMessage={DEFAULT_ACTION_MESSAGE} + toastNotifications={notifications.toasts} + /> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx new file mode 100644 index 0000000000000..6f3d299da8d45 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCard, + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { FieldHook } from '../../../../../shared_imports'; +import { useKibana } from '../../../../../lib/kibana'; +import * as i18n from './translations'; + +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( + <EuiText size="s"> + {hasValidLicense ? ( + i18n.ML_TYPE_DESCRIPTION + ) : ( + <FormattedMessage + id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription" + defaultMessage="Access to ML requires a {subscriptionsLink}." + values={{ + subscriptionsLink: ( + <EuiLink href={subscriptionUrl} target="_blank"> + <FormattedMessage + id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink" + defaultMessage="Platinum subscription" + /> + </EuiLink> + ), + }} + /> + )} + </EuiText> +); + +interface SelectRuleTypeProps { + describedByIds?: string[]; + field: FieldHook; + hasValidLicense?: boolean; + isMlAdmin?: boolean; + isReadOnly?: boolean; +} + +export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ + describedByIds = [], + field, + isReadOnly = false, + hasValidLicense = false, + isMlAdmin = false, +}) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/elasticsearch/license_management', + }); + + return ( + <EuiFormRow + fullWidth + data-test-subj="selectRuleType" + describedByIds={describedByIds} + label={field.label} + > + <EuiFlexGrid columns={4}> + <EuiFlexItem> + <EuiCard + data-test-subj="customRuleType" + title={i18n.QUERY_TYPE_TITLE} + description={i18n.QUERY_TYPE_DESCRIPTION} + icon={<EuiIcon size="l" type="search" />} + selectable={{ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiCard + data-test-subj="machineLearningRuleType" + title={i18n.ML_TYPE_TITLE} + description={ + <MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} /> + } + icon={<EuiIcon size="l" type="machineLearningApp" />} + isDisabled={mlCardDisabled} + selectable={{ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + </EuiFlexItem> + </EuiFlexGrid> + </EuiFormRow> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx new file mode 100644 index 0000000000000..b6887badc56be --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import React, { FC, memo, useCallback, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; +import { useUiSetting$ } from '../../../../../lib/kibana'; +import { setFieldValue } from '../../helpers'; +import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; +import { StepRuleDescription } from '../description_step'; +import { QueryBarDefineRule } from '../query_bar'; +import { SelectRuleType } from '../select_rule_type'; +import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; +import { MlJobSelect } from '../ml_job_select'; +import { PickTimeline } from '../pick_timeline'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { NextStep } from '../next_step'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, + FormSchema, +} from '../../../../../shared_imports'; +import { schema } from './schema'; +import * as i18n from './translations'; +import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; +import { hasMlAdminPermissions } from '../../../../../components/ml/permissions/has_ml_admin_permissions'; + +const CommonUseField = getUseField({ component: Field }); + +interface StepDefineRuleProps extends RuleStepProps { + defaultValues?: DefineStepRule | null; +} + +const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, + index: [], + isNew: true, + machineLearningJobId: '', + ruleType: 'query', + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: null, + title: DEFAULT_TIMELINE_TITLE, + }, +}; + +const MyLabelButton = styled(EuiButtonEmpty)` + height: 18px; + font-size: 12px; + + .euiIcon { + width: 14px; + height: 14px; + } +`; + +MyLabelButton.defaultProps = { + flush: 'right', +}; + +const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ + addPadding = false, + defaultValues, + descriptionColumns = 'singleSplit', + isReadOnlyView, + isLoading, + isUpdateView = false, + setForm, + setStepData, +}) => { + const mlCapabilities = useMlCapabilities(); + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [indexModified, setIndexModified] = useState(false); + const [localIsMlRule, setIsMlRule] = useState(false); + const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [myStepData, setMyStepData] = useState<DefineStepRule>({ + ...stepDefineDefaultValue, + index: indicesConfig ?? [], + }); + const [ + { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + ] = useFetchIndexPatterns(myStepData.index); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.defineRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid && setStepData) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...values } = myStepData; + if (defaultValues != null && !deepEqual(values, defaultValues)) { + const newValues = { ...values, ...defaultValues, isNew: false }; + setMyStepData(newValues); + setFieldValue(form, schema, newValues); + } + }, [defaultValues, setMyStepData, setFieldValue]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.defineRule, form); + } + }, [form]); + + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); + + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); + + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); + + return isReadOnlyView ? ( + <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}> + <StepRuleDescription + columns={descriptionColumns} + indexPatterns={indexPatternQueryBar as IIndexPattern} + schema={filterRuleFieldsForType(schema as FormSchema & RuleFields, myStepData.ruleType)} + data={filterRuleFieldsForType(myStepData, myStepData.ruleType)} + /> + </StepContentWrapper> + ) : ( + <> + <StepContentWrapper addPadding={!isUpdateView}> + <Form form={form} data-test-subj="stepDefineRule"> + <UseField + path="ruleType" + component={SelectRuleType} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRuleType'], + isReadOnly: isUpdateView, + hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, + isMlAdmin: hasMlAdminPermissions(mlCapabilities), + }} + /> + <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'none' : 'flex' }}> + <> + <CommonUseField + path="index" + config={{ + ...schema.index, + labelAppend: indexModified ? ( + <MyLabelButton onClick={handleResetIndices} iconType="refresh"> + {i18n.RESET_DEFAULT_INDEX} + </MyLabelButton> + ) : null, + }} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleIndices', + 'data-test-subj': 'detectionEngineStepDefineRuleIndices', + euiFieldProps: { + fullWidth: true, + isDisabled: isLoading, + placeholder: '', + }, + }} + /> + <UseField + path="queryBar" + config={{ + ...schema.queryBar, + labelAppend: ( + <MyLabelButton onClick={handleOpenTimelineSearch}> + {i18n.IMPORT_TIMELINE_QUERY} + </MyLabelButton> + ), + }} + component={QueryBarDefineRule} + componentProps={{ + browserFields, + idAria: 'detectionEngineStepDefineRuleQueryBar', + indexPattern: indexPatternQueryBar, + isDisabled: isLoading, + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, + }} + /> + </> + </EuiFormRow> + <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}> + <> + <UseField + path="machineLearningJobId" + component={MlJobSelect} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'], + }} + /> + <UseField + path="anomalyThreshold" + component={AnomalyThresholdSlider} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'], + }} + /> + </> + </EuiFormRow> + <UseField + path="timeline" + component={PickTimeline} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleTimeline', + isDisabled: isLoading, + dataTestSubj: 'detectionEngineStepDefineRuleTimeline', + }} + /> + <FormDataProvider pathsToWatch={['index', 'ruleType']}> + {({ index, ruleType }) => { + if (index != null) { + if (deepEqual(index, indicesConfig) && indexModified) { + setIndexModified(false); + } else if (!deepEqual(index, indicesConfig) && !indexModified) { + setIndexModified(true); + } + } + + if (isMlRule(ruleType) && !localIsMlRule) { + setIsMlRule(true); + clearErrors(); + } else if (!isMlRule(ruleType) && localIsMlRule) { + setIsMlRule(false); + clearErrors(); + } + + return null; + }} + </FormDataProvider> + </Form> + </StepContentWrapper> + + {!isUpdateView && ( + <NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} /> + )} + </> + ); +}; + +export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx new file mode 100644 index 0000000000000..8915c5f0a224f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { esKuery } from '../../../../../../../../../src/plugins/data/public'; +import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../../../../../shared_imports'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; + +export const schema: FormSchema = { + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', + { + defaultMessage: 'Index patterns', + } + ), + helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>, + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ value, path, formData }] = args; + const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ value, path, formData }] = args; + const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + esKuery.fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); + }, + }, + ], + }, + timeline: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: + 'Select an existing timeline to use as a template when investigating generated signals.', + } + ), + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx new file mode 100644 index 0000000000000..0cf15c41a0f91 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; + +import { + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_THROTTLE_NO_ACTIONS, +} from '../../../../../../common/constants'; +import { SelectField } from '../../../../../shared_imports'; + +export const THROTTLE_OPTIONS = [ + { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, + { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, + { value: '1h', text: 'Hourly' }, + { value: '1d', text: 'Daily' }, + { value: '7d', text: 'Weekly' }, +]; + +type ThrottleSelectField = typeof SelectField; + +export const ThrottleSelectField: ThrottleSelectField = props => { + const onChange = useCallback( + e => { + const throttle = e.target.value; + props.field.setValue(throttle); + props.handleChange(throttle); + }, + [props.field.setValue, props.handleChange] + ); + const newEuiFieldProps = { ...props.euiFieldProps, onChange }; + return <SelectField {...props} euiFieldProps={newEuiFieldProps} />; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts new file mode 100644 index 0000000000000..7ad116c313361 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { has, isEmpty } from 'lodash/fp'; +import moment from 'moment'; +import deepmerge from 'deepmerge'; + +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; +import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; +import { NewRule } from '../../../../containers/detection_engine/rules'; + +import { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + ActionsStepRuleJson, +} from '../types'; + +export const getTimeTypeValue = (time: string): { unit: string; value: number } => { + const timeObj = { + unit: '', + value: 0, + }; + const filterTimeVal = (time as string).match(/\d+/g); + const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { + timeObj.value = Number(filterTimeVal[0]); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) + ) { + timeObj.unit = filterTimeType[0]; + } + return timeObj; +}; + +export interface RuleFields { + anomalyThreshold: unknown; + machineLearningJobId: unknown; + queryBar: unknown; + index: unknown; + ruleType: unknown; +} +type QueryRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId'>; +type MlRuleFields<T> = Omit<T, 'queryBar' | 'index'>; + +const isMlFields = <T>(fields: QueryRuleFields<T> | MlRuleFields<T>): fields is MlRuleFields<T> => + has('anomalyThreshold', fields); + +export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: RuleType) => { + if (isMlRule(type)) { + const { index, queryBar, ...mlRuleFields } = fields; + return mlRuleFields; + } else { + const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + return queryRuleFields; + } +}; + +export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { + const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + const { ruleType, timeline } = ruleFields; + const baseFields = { + type: ruleType, + ...(timeline.id != null && + timeline.title != null && { + timeline_id: timeline.id, + timeline_title: timeline.title, + }), + }; + + const typeFields = isMlFields(ruleFields) + ? { + anomaly_threshold: ruleFields.anomalyThreshold, + machine_learning_job_id: ruleFields.machineLearningJobId, + } + : { + index: ruleFields.index, + filters: ruleFields.queryBar?.filters, + language: ruleFields.queryBar?.query?.language, + query: ruleFields.queryBar?.query?.query as string, + saved_id: ruleFields.queryBar?.saved_id, + ...(ruleType === 'query' && + ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), + }; + + return { + ...baseFields, + ...typeFields, + }; +}; + +export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { + const { isNew, ...formatScheduleData } = scheduleData; + if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( + formatScheduleData.interval + ); + const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); + formatScheduleData.from = `now-${duration.asSeconds()}s`; + formatScheduleData.to = 'now'; + } + return { + ...formatScheduleData, + meta: { + from: scheduleData.from, + }, + }; +}; + +export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; + return { + false_positives: falsePositives.filter(item => !isEmpty(item)), + references: references.filter(item => !isEmpty(item)), + risk_score: riskScore, + threat: threat + .filter(singleThreat => singleThreat.tactic.name !== 'none') + .map(singleThreat => ({ + ...singleThreat, + framework: 'MITRE ATT&CK', + technique: singleThreat.technique.map(technique => { + const { id, name, reference } = technique; + return { id, name, reference }; + }), + })), + ...(!isEmpty(note) ? { note } : {}), + ...rest, + }; +}; + +export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { + const { + actions = [], + enabled, + kibanaSiemAppUrl, + throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, + } = actionsStepData; + + return { + actions: actions.map(transformAlertToRuleAction), + enabled, + throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, + meta: { + kibana_siem_app_url: kibanaSiemAppUrl, + }, + }; +}; + +export const formatRule = ( + defineStepData: DefineStepRule, + aboutStepData: AboutStepRule, + scheduleData: ScheduleStepRule, + actionsData: ActionsStepRule +): NewRule => + deepmerge.all([ + formatDefineStepData(defineStepData), + formatAboutStepData(aboutStepData), + formatScheduleStepData(scheduleData), + formatActionsStepData(actionsData), + ]) as NewRule; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx new file mode 100644 index 0000000000000..f2a04a87ced27 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -0,0 +1,378 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + GetStepsData, + getDefineStepsData, + getScheduleStepsData, + getStepsData, + getAboutStepsData, + getActionsStepsData, + getHumanizedDuration, + getModifiedAboutDetailsData, + determineDetailsValue, + userHasNoPermissions, +} from './helpers'; +import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +describe('rule helpers', () => { + describe('getStepsData', () => { + test('returns object with about, define, schedule and actions step properties formatted', () => { + const { + defineRuleData, + modifiedAboutRuleDetailsData, + aboutRuleData, + scheduleRuleData, + ruleActionsData, + }: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + }); + const defineRuleStepData = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + index: ['auditbeat-*'], + machineLearningJobId: '', + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const aboutRuleStepData = { + description: '24/7', + falsePositives: ['test'], + isNew: false, + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; + const ruleActionsStepData = { + enabled: true, + throttle: 'no_actions', + isNew: false, + actions: [], + }; + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(ruleActionsData).toEqual(ruleActionsStepData); + expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); + }); + }); + + describe('getAboutStepsData', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); + + expect(result.name).toEqual(''); + expect(result.description).toEqual(''); + expect(result.note).toEqual(''); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.note).toEqual(''); + }); + }); + + describe('determineDetailsValue', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockRuleWithEverything('test-id'), + true + ); + const expected = { name: '', description: '', note: '' }; + + expect(result).toEqual(expected); + }); + + test('returns name, description, and note values if detailsView is false', () => { + const mockedRule = mockRuleWithEverything('test-id'); + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockedRule, + false + ); + const expected = { + name: mockedRule.name, + description: mockedRule.description, + note: mockedRule.note, + }; + + expect(result).toEqual(expected); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockedRule, + false + ); + const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; + + expect(result).toEqual(expected); + }); + }); + + describe('getDefineStepsData', () => { + test('returns with saved_id if value exists on rule', () => { + const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: "Garrett's IP", + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns with saved_id of undefined if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: DefineStepRule = getDefineStepsData(mockedRule); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: DefineStepRule = getDefineStepsData(mockedRule); + + expect(result.timeline.id).toBeNull(); + expect(result.timeline.title).toBeNull(); + }); + }); + + describe('getHumanizedDuration', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const result = getHumanizedDuration('now-62s', '1m'); + + expect(result).toEqual('2s'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const result = getHumanizedDuration('now-660s', '5m'); + + expect(result).toEqual('6m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const result = getHumanizedDuration('now-7400s', '5m'); + + expect(result).toEqual('1h'); + }); + + test('returns from as if from is not parsable as dateMath', () => { + const result = getHumanizedDuration('randomstring', '5m'); + + expect(result).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const result = getHumanizedDuration('now-300s', 'randomstring'); + + expect(result).toEqual('5m'); + }); + }); + + describe('getScheduleStepsData', () => { + test('returns expected ScheduleStep rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + const result: ScheduleStepRule = getScheduleStepsData(mockedRule); + const expected = { + isNew: false, + interval: mockedRule.interval, + from: '0s', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getActionsStepsData', () => { + test('returns expected ActionsStepRule rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + actions: [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + }, + ], + }; + const result: ActionsStepRule = getActionsStepsData(mockedRule); + const expected = { + actions: [ + { + id: 'id', + group: 'group', + params: {}, + actionTypeId: 'action_type_id', + }, + ], + enabled: mockedRule.enabled, + isNew: false, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getModifiedAboutDetailsData', () => { + test('returns object with "note" and "description" being those of passed in rule', () => { + const result: AboutStepRuleDetails = getModifiedAboutDetailsData( + mockRuleWithEverything('test-id') + ); + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(result).toEqual(aboutRuleDataDetailsData); + }); + + test('returns "note" with empty string if "note" does not exist', () => { + const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; + const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); + + const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + + expect(result).toEqual(aboutRuleDetailsData); + }); + }); + + describe('userHasNoPermissions', () => { + test("returns false when user's CRUD operations are null", () => { + const result: boolean = userHasNoPermissions(null); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns true when user cannot CRUD', () => { + const result: boolean = userHasNoPermissions(false); + const userHasNoPermissionsExpectedResult = true; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns false when user can CRUD', () => { + const result: boolean = userHasNoPermissions(true); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx new file mode 100644 index 0000000000000..2ccbffd864070 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { get } from 'lodash/fp'; +import moment from 'moment'; +import memoizeOne from 'memoize-one'; +import { useLocation } from 'react-router-dom'; + +import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { FormData, FormHook, FormSchema } from '../../../shared_imports'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + IMitreEnterpriseAttack, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +export interface GetStepsData { + aboutRuleData: AboutStepRule; + modifiedAboutRuleDetailsData: AboutStepRuleDetails; + defineRuleData: DefineStepRule; + scheduleRuleData: ScheduleStepRule; + ruleActionsData: ActionsStepRule; +} + +export const getStepsData = ({ + rule, + detailsView = false, +}: { + rule: Rule; + detailsView?: boolean; +}): GetStepsData => { + const defineRuleData: DefineStepRule = getDefineStepsData(rule); + const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); + const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); + const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); + const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); + + return { + aboutRuleData, + modifiedAboutRuleDetailsData, + defineRuleData, + scheduleRuleData, + ruleActionsData, + }; +}; + +export const getActionsStepsData = ( + rule: Omit<Rule, 'actions'> & { actions: RuleAlertAction[] } +): ActionsStepRule => { + const { enabled, throttle, meta, actions = [] } = rule; + + return { + actions: actions?.map(transformRuleToAlertAction), + isNew: false, + throttle, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, + enabled, + }; +}; + +export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ + isNew: false, + ruleType: rule.type, + anomalyThreshold: rule.anomaly_threshold ?? 50, + machineLearningJobId: rule.machine_learning_job_id ?? '', + index: rule.index ?? [], + queryBar: { + query: { query: rule.query ?? '', language: rule.language ?? '' }, + filters: (rule.filters ?? []) as Filter[], + saved_id: rule.saved_id, + }, + timeline: { + id: rule.timeline_id ?? null, + title: rule.timeline_title ?? null, + }, +}); + +export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { + const { interval, from } = rule; + const fromHumanizedValue = getHumanizedDuration(from, interval); + + return { + isNew: false, + interval, + from: fromHumanizedValue, + }; +}; + +export const getHumanizedDuration = (from: string, interval: string): string => { + const fromValue = dateMath.parse(from) ?? moment(); + const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); + + const fromDuration = moment.duration(intervalValue.diff(fromValue)); + const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; + + if (fromDuration.asSeconds() < 60) { + return `${Math.floor(fromDuration.asSeconds())}s`; + } else if (fromDuration.asMinutes() < 60) { + return `${Math.floor(fromDuration.asMinutes())}m`; + } + + return fromHumanize; +}; + +export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { + const { name, description, note } = determineDetailsValue(rule, detailsView); + const { + references, + severity, + false_positives: falsePositives, + risk_score: riskScore, + tags, + threat, + } = rule; + + return { + isNew: false, + name, + description, + note: note!, + references, + severity, + tags, + riskScore, + falsePositives, + threat: threat as IMitreEnterpriseAttack[], + }; +}; + +export const determineDetailsValue = ( + rule: Rule, + detailsView: boolean +): Pick<Rule, 'name' | 'description' | 'note'> => { + const { name, description, note } = rule; + if (detailsView) { + return { name: '', description: '', note: '' }; + } + + return { name, description, note: note ?? '' }; +}; + +export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ + note: rule.note ?? '', + description: rule.description, +}); + +export const useQuery = () => new URLSearchParams(useLocation().search); + +export type PrePackagedRuleStatus = + | 'ruleInstalled' + | 'ruleNotInstalled' + | 'ruleNeedUpdate' + | 'someRuleUninstall' + | 'unknown'; + +export const getPrePackagedRuleStatus = ( + rulesInstalled: number | null, + rulesNotInstalled: number | null, + rulesNotUpdated: number | null +): PrePackagedRuleStatus => { + if ( + rulesNotInstalled != null && + rulesInstalled === 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'ruleNotInstalled'; + } else if ( + rulesInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled === 0 && + rulesNotUpdated === 0 + ) { + return 'ruleInstalled'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'someRuleUninstall'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesNotUpdated != null && + rulesInstalled > 0 && + rulesNotInstalled >= 0 && + rulesNotUpdated > 0 + ) { + return 'ruleNeedUpdate'; + } + return 'unknown'; +}; +export const setFieldValue = ( + form: FormHook<FormData>, + schema: FormSchema<FormData>, + defaultValues: unknown +) => + Object.keys(schema).forEach(key => { + const val = get(key, defaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); + +export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { + const commonRuleParamsKeys = [ + 'id', + 'name', + 'description', + 'false_positives', + 'rule_id', + 'max_signals', + 'risk_score', + 'output_index', + 'references', + 'severity', + 'timeline_id', + 'timeline_title', + 'threat', + 'type', + 'version', + // 'lists', + ]; + + const ruleParamsKeys = [ + ...commonRuleParamsKeys, + ...(isMlRule(ruleType) + ? ['anomaly_threshold', 'machine_learning_job_id'] + : ['index', 'filters', 'language', 'query', 'saved_id']), + ].sort(); + + return ruleParamsKeys; +}; + +export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { + if (!ruleType) { + return []; + } + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + 'state.signals_count', + '{context.results_link}', + ...actionMessageRuleParams.map(param => `context.rule.${param}`), + ]; +}); + +// typed as null not undefined as the initial state for this value is null. +export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => + canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..dcb5397d28f7c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; +import { AlertAction } from '../../../../../alerting/common'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { FieldValueQueryBar } from './components/query_bar'; +import { FormData, FormHook } from '../../../shared_imports'; +import { FieldValueTimeline } from './components/pick_timeline'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort?: EuiBasicTableSortTypes; +} + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', + ruleActions = 'rule-actions', +} +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStepData { + data: unknown; + isValid: boolean; +} + +export interface RuleStepProps { + addPadding?: boolean; + descriptionColumns?: 'multi' | 'single' | 'singleSplit'; + setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; + isReadOnlyView: boolean; + isUpdateView?: boolean; + isLoading: boolean; + resizeParentContainer?: (height: number) => void; + setForm?: (step: RuleStep, form: FormHook<FormData>) => void; +} + +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note: string; +} + +export interface AboutStepRuleDetails { + note: string; + description: string; +} + +export interface DefineStepRule extends StepRuleData { + anomalyThreshold: number; + index: string[]; + machineLearningJobId: string; + queryBar: FieldValueQueryBar; + ruleType: RuleType; + timeline: FieldValueTimeline; +} + +export interface ScheduleStepRule extends StepRuleData { + interval: string; + from: string; + to?: string; +} + +export interface ActionsStepRule extends StepRuleData { + actions: AlertAction[]; + enabled: boolean; + kibanaSiemAppUrl?: string; + throttle?: string | null; +} + +export interface DefineStepRuleJson { + anomaly_threshold?: number; + index?: string[]; + filters?: Filter[]; + machine_learning_job_id?: string; + saved_id?: string; + query?: string; + language?: string; + timeline_id?: string; + timeline_title?: string; + type: RuleType; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note?: string; +} + +export interface ScheduleStepRuleJson { + interval: string; + from: string; + to?: string; + meta?: unknown; +} + +export interface ActionsStepRuleJson { + actions: RuleAlertAction[]; + enabled: boolean; + throttle?: string | null; + meta?: unknown; +} + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} +export interface IMitreEnterpriseAttack { + framework: string; + tactic: IMitreAttack; + technique: IMitreAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts new file mode 100644 index 0000000000000..f93ad94dd462b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { + getDetectionEngineUrl, + getDetectionEngineTabUrl, + getRulesUrl, + getRuleDetailsUrl, + getCreateRuleUrl, + getEditRuleUrl, +} from '../../../components/link_to/redirect_to_detection_engine'; +import * as i18nDetections from '../translations'; +import * as i18nRules from './translations'; +import { RouteSpyState } from '../../../utils/route/types'; + +const getTabBreadcrumb = (pathname: string, search: string[]) => { + const tabPath = pathname.split('/')[2]; + + if (tabPath === 'alerts') { + return { + text: i18nDetections.ALERT, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'signals') { + return { + text: i18nDetections.SIGNAL, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'rules') { + return { + text: i18nRules.PAGE_TITLE, + href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/edit'); + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18nDetections.PAGE_TITLE, + href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + + if (tabBreadcrumb) { + breadcrumb = [...breadcrumb, tabBreadcrumb]; + } + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/plugins/siem/public/pages/home/home_navigations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx rename to x-pack/plugins/siem/public/pages/home/home_navigations.tsx diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx new file mode 100644 index 0000000000000..a9e0962f16e6e --- /dev/null +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import styled from 'styled-components'; + +import { useThrottledResizeObserver } from '../../components/utils'; +import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; +import { Flyout } from '../../components/flyout'; +import { HeaderGlobal } from '../../components/header_global'; +import { HelpMenu } from '../../components/help_menu'; +import { LinkToPage } from '../../components/link_to'; +import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; +import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; +import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; +import { UseUrlState } from '../../components/url_state'; +import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; +import { NotFoundPage } from '../404'; +import { DetectionEngineContainer } from '../detection_engine'; +import { HostsContainer } from '../hosts'; +import { NetworkContainer } from '../network'; +import { Overview } from '../overview'; +import { Case } from '../case'; +import { Timelines } from '../timelines'; +import { navTabs } from './home_navigations'; +import { SiemPageName } from './types'; + +const WrappedByAutoSizer = styled.div` + height: 100%; +`; +WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; + +const Main = styled.main` + height: 100%; +`; +Main.displayName = 'Main'; + +const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) + +/** the global Kibana navigation at the top of every page */ +const globalHeaderHeightPx = 48; + +const calculateFlyoutHeight = ({ + globalHeaderSize, + windowHeight, +}: { + globalHeaderSize: number; + windowHeight: number; +}): number => Math.max(0, windowHeight - globalHeaderSize); + +export const HomePage: React.FC = () => { + const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); + const flyoutHeight = useMemo( + () => + calculateFlyoutHeight({ + globalHeaderSize: globalHeaderHeightPx, + windowHeight, + }), + [windowHeight] + ); + + const [showTimeline] = useShowTimeline(); + + return ( + <WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" ref={measureRef}> + <HeaderGlobal /> + + <Main data-test-subj="pageContainer"> + <WithSource sourceId="default"> + {({ browserFields, indexPattern, indicesExist }) => ( + <DragDropContextWrapper browserFields={browserFields}> + <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> + {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( + <> + <AutoSaveWarningMsg /> + <Flyout + flyoutHeight={flyoutHeight} + timelineId="timeline-1" + usersViewing={usersViewing} + /> + </> + )} + + <Switch> + <Redirect exact from="/" to={`/${SiemPageName.overview}`} /> + <Route path={`/:pageName(${SiemPageName.overview})`} render={() => <Overview />} /> + <Route + path={`/:pageName(${SiemPageName.hosts})`} + render={({ match }) => <HostsContainer url={match.url} />} + /> + <Route + path={`/:pageName(${SiemPageName.network})`} + render={({ location, match }) => ( + <NetworkContainer location={location} url={match.url} /> + )} + /> + <Route + path={`/:pageName(${SiemPageName.detections})`} + render={({ location, match }) => ( + <DetectionEngineContainer location={location} url={match.url} /> + )} + /> + <Route + path={`/:pageName(${SiemPageName.timelines})`} + render={() => <Timelines />} + /> + <Route path="/link-to" render={props => <LinkToPage {...props} />} /> + <Route + path="/ml-hosts" + render={({ location, match }) => ( + <MlHostConditionalContainer location={location} url={match.url} /> + )} + /> + <Route + path="/ml-network" + render={({ location, match }) => ( + <MlNetworkConditionalContainer location={location} url={match.url} /> + )} + /> + <Route path={`/:pageName(${SiemPageName.case})`}> + <Case /> + </Route> + <Route render={() => <NotFoundPage />} /> + </Switch> + </DragDropContextWrapper> + )} + </WithSource> + </Main> + + <HelpMenu /> + + <SpyRoute /> + </WrappedByAutoSizer> + ); +}; + +HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/plugins/siem/public/pages/home/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/translations.ts rename to x-pack/plugins/siem/public/pages/home/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/plugins/siem/public/pages/home/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/types.ts rename to x-pack/plugins/siem/public/pages/home/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts b/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts new file mode 100644 index 0000000000000..d989d709cc4af --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getHostDetailsEventsKqlQueryExpression, getHostDetailsPageFilters } from './helpers'; +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; + +describe('hosts page helpers', () => { + describe('getHostDetailsEventsKqlQueryExpression', () => { + const filterQueryExpression = 'user.name: "root"'; + const hostName = 'foo'; + + it('combines the filterQueryExpression and hostname when both are NOT empty', () => { + expect(getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName })).toEqual( + 'user.name: "root" and host.name: "foo"' + ); + }); + + it('returns just the filterQueryExpression when it is NOT empty, but hostname is empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName: '' }) + ).toEqual('user.name: "root"'); + }); + + it('returns just the hostname when filterQueryExpression is empty, but hostname is NOT empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName }) + ).toEqual('host.name: "foo"'); + }); + + it('returns an empty string when both the filterQueryExpression and hostname are empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName: '' }) + ).toEqual(''); + }); + }); + + describe('getHostDetailsPageFilters', () => { + it('correctly constructs pageFilters for the given hostName', () => { + const expected: Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: 'host-1', + params: { + query: 'host-1', + }, + }, + query: { + match: { + 'host.name': { + query: 'host-1', + type: 'phrase', + }, + }, + }, + }, + ]; + expect(getHostDetailsPageFilters('host-1')).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts b/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts new file mode 100644 index 0000000000000..6da76f2fb5cac --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { escapeQueryValue } from '../../../lib/keury'; +import { Filter } from '../../../../../../../src/plugins/data/public'; + +/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ +export const getHostDetailsEventsKqlQueryExpression = ({ + filterQueryExpression, + hostName, +}: { + filterQueryExpression: string; + hostName: string; +}): string => { + if (filterQueryExpression.length) { + return `${filterQueryExpression}${ + hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' + }`; + } else { + return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; + } +}; + +export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: hostName, + params: { + query: hostName, + }, + }, + query: { + match: { + 'host.name': { + query: hostName, + type: 'phrase', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/plugins/siem/public/pages/hosts/details/index.tsx new file mode 100644 index 0000000000000..ef04288aa1b6f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/index.tsx @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; +import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../components/ml_popover/hooks/use_ml_capabilities'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { SiemNavigation } from '../../../components/navigation'; +import { KpiHostsComponent } from '../../../components/page/hosts'; +import { HostOverview } from '../../../components/page/hosts/host_overview'; +import { manageQuery } from '../../../components/page/manage_query'; +import { SiemSearchBar } from '../../../components/search_bar'; +import { WrapperPage } from '../../../components/wrapper_page'; +import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; +import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { inputsSelectors, State } from '../../../store'; +import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; + +import { HostsEmptyPage } from '../hosts_empty_page'; +import { HostDetailsTabs } from './details_tabs'; +import { navTabsHostDetails } from './nav_tabs'; +import { HostDetailsProps } from './types'; +import { type } from './utils'; +import { getHostDetailsPageFilters } from './helpers'; + +const HostOverviewManage = manageQuery(HostOverview); +const KpiHostDetailsManage = manageQuery(KpiHostsComponent); + +const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>( + ({ + filters, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero, + setQuery, + to, + detailName, + deleteQuery, + hostDetailsPagePath, + }) => { + useEffect(() => { + setHostDetailsTablesActivePageToZero(); + }, [setHostDetailsTablesActivePageToZero, detailName]); + const capabilities = useMlCapabilities(); + const kibana = useKibana(); + const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ + detailName, + ]); + const getFilters = () => [...hostDetailsPageFilters, ...filters]; + const narrowDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + <> + <WithSource sourceId="default"> + {({ indicesExist, indexPattern }) => { + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <StickyContainer> + <FiltersGlobal> + <SiemSearchBar indexPattern={indexPattern} id="global" /> + </FiltersGlobal> + + <WrapperPage> + <HeaderPage + border + subtitle={ + <LastEventTime + indexKey={LastEventIndexKey.hostDetails} + hostName={detailName} + /> + } + title={detailName} + /> + + <HostOverviewByNameQuery + sourceId="default" + hostName={detailName} + skip={isInitializing} + startDate={from} + endDate={to} + > + {({ hostOverview, loading, id, inspect, refetch }) => ( + <AnomalyTableProvider + criteriaFields={hostToCriteria(hostOverview)} + startDate={from} + endDate={to} + skip={isInitializing} + > + {({ isLoadingAnomaliesData, anomaliesData }) => ( + <HostOverviewManage + id={id} + inspect={inspect} + refetch={refetch} + setQuery={setQuery} + data={hostOverview} + anomaliesData={anomaliesData} + isLoadingAnomaliesData={isLoadingAnomaliesData} + loading={loading} + startDate={from} + endDate={to} + narrowDateRange={(score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + </AnomalyTableProvider> + )} + </HostOverviewByNameQuery> + + <EuiHorizontalRule /> + + <KpiHostDetailsQuery + sourceId="default" + filterQuery={filterQuery} + skip={isInitializing} + startDate={from} + endDate={to} + > + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + <KpiHostDetailsManage + data={kpiHostDetails} + from={from} + id={id} + inspect={inspect} + loading={loading} + refetch={refetch} + setQuery={setQuery} + to={to} + narrowDateRange={narrowDateRange} + /> + )} + </KpiHostDetailsQuery> + + <EuiSpacer /> + + <SiemNavigation + navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))} + /> + + <EuiSpacer /> + + <HostDetailsTabs + isInitializing={isInitializing} + deleteQuery={deleteQuery} + pageFilters={hostDetailsPageFilters} + to={to} + from={from} + detailName={detailName} + type={type} + setQuery={setQuery} + filterQuery={filterQuery} + hostDetailsPagePath={hostDetailsPagePath} + indexPattern={indexPattern} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + /> + </WrapperPage> + </StickyContainer> + ) : ( + <WrapperPage> + <HeaderPage border title={detailName} /> + + <HostsEmptyPage /> + </WrapperPage> + ); + }} + </WithSource> + + <SpyRoute /> + </> + ); + } +); +HostDetailsComponent.displayName = 'HostDetailsComponent'; + +export const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts b/x-pack/plugins/siem/public/pages/hosts/details/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts rename to x-pack/plugins/siem/public/pages/hosts/details/types.ts diff --git a/x-pack/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/plugins/siem/public/pages/hosts/details/utils.ts new file mode 100644 index 0000000000000..af4ba8eb091e2 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { hostsModel } from '../../../store'; +import { HostsTableType } from '../../../store/hosts/model'; +import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; + +import * as i18n from '../translations'; +import { HostRouteSpyState } from '../../../utils/route/types'; + +export const type = hostsModel.HostsType.details; + +const TabNameMappedToI18nKey: Record<HostsTableType, string> = { + [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, + [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, + [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, +}; + +export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx index 99cf767c65e08..6134c1dd6911a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; -import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import '../../mock/match_media'; import { mocksSource } from '../../containers/source/mock'; import { wait } from '../../lib/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts.tsx index d574925a91600..028c804f4d48f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx @@ -28,7 +28,7 @@ import { inputsSelectors, State, hostsModel } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { esQuery } from '../../../../../../src/plugins/data/public'; import { useMlCapabilities } from '../../components/ml_popover/hooks/use_ml_capabilities'; import { HostsEmptyPage } from './hosts_empty_page'; import { HostsTabs } from './hosts_tabs'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/plugins/siem/public/pages/hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx rename to x-pack/plugins/siem/public/pages/hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..ec33834b1bf73 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { AlertsComponentQueryProps } from './types'; + +export const filterHostData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', + }, + }, +]; +export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { + const { pageFilters, ...rest } = alertsProps; + const hostPageFilters = useMemo( + () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), + [pageFilters] + ); + + return <AlertsView {...rest} pageFilters={hostPageFilters} />; +}); + +HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/plugins/siem/public/pages/hosts/navigation/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts rename to x-pack/plugins/siem/public/pages/hosts/navigation/index.ts diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts new file mode 100644 index 0000000000000..20d4d4e463a7f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { InspectQuery, Refetch } from '../../../store/inputs/model'; + +import { HostsTableType, HostsType } from '../../../store/hosts/model'; +import { NavTab } from '../../../components/navigation/types'; +import { UpdateDateRange } from '../../../components/charts/common'; + +export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & + HostsTableType.authentications & + HostsTableType.uncommonProcesses & + HostsTableType.events; + +type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; + +type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; + +export type HostsNavTab = Record<KeyHostsNavTab, NavTab>; + +export type SetQuery = ({ + id, + inspect, + loading, + refetch, +}: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; +}) => void; + +export interface QueryTabBodyProps { + type: HostsType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; +} + +export type HostsComponentsQueryProps = QueryTabBodyProps & { + deleteQuery?: ({ id }: { id: string }) => void; + indexPattern: IIndexPattern; + pageFilters?: Filter[]; + skip: boolean; + setQuery: SetQuery; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; +}; + +export type AlertsComponentQueryProps = HostsComponentsQueryProps & { + filterQuery: string; + pageFilters?: Filter[]; +}; + +export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts b/x-pack/plugins/siem/public/pages/hosts/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts rename to x-pack/plugins/siem/public/pages/hosts/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts b/x-pack/plugins/siem/public/pages/hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/types.ts rename to x-pack/plugins/siem/public/pages/hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/index.tsx b/x-pack/plugins/siem/public/pages/network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/index.tsx rename to x-pack/plugins/siem/public/pages/network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx new file mode 100644 index 0000000000000..350d6e34c1c0f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx @@ -0,0 +1,298 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { networkToCriteria } from '../../../components/ml/criteria/network_to_criteria'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; +import { manageQuery } from '../../../components/page/manage_query'; +import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected'; +import { IpOverview } from '../../../components/page/network/ip_overview'; +import { SiemSearchBar } from '../../../components/search_bar'; +import { WrapperPage } from '../../../components/wrapper_page'; +import { IpOverviewQuery } from '../../../containers/ip_overview'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../lib/kibana'; +import { decodeIpv6 } from '../../../lib/helpers'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group'; +import { networkModel, State, inputsSelectors } from '../../../store'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { NetworkEmptyPage } from '../network_empty_page'; +import { NetworkHttpQueryTable } from './network_http_query_table'; +import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; +import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; +import { TlsQueryTable } from './tls_query_table'; +import { IPDetailsComponentProps } from './types'; +import { UsersQueryTable } from './users_query_table'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; + +export { getBreadcrumbs } from './utils'; + +const IpOverviewManage = manageQuery(IpOverview); + +export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRedux> = ({ + detailName, + filters, + flowTarget, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero, + setQuery, + to, +}) => { + const type = networkModel.NetworkType.details; + const narrowDateRange = useCallback( + (score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + const kibana = useKibana(); + + useEffect(() => { + setIpDetailsTablesActivePageToZero(); + }, [detailName, setIpDetailsTablesActivePageToZero]); + + return ( + <> + <WithSource sourceId="default" data-test-subj="ip-details-page"> + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <StickyContainer> + <FiltersGlobal> + <SiemSearchBar indexPattern={indexPattern} id="global" /> + </FiltersGlobal> + + <WrapperPage> + <HeaderPage + border + data-test-subj="ip-details-headline" + draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} + subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />} + title={ip} + > + <FlowTargetSelectConnected flowTarget={flowTarget} /> + </HeaderPage> + + <IpOverviewQuery + skip={isInitializing} + sourceId="default" + filterQuery={filterQuery} + type={type} + ip={ip} + > + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + <AnomalyTableProvider + criteriaFields={networkToCriteria(detailName, flowTarget)} + startDate={from} + endDate={to} + skip={isInitializing} + > + {({ isLoadingAnomaliesData, anomaliesData }) => ( + <IpOverviewManage + id={id} + inspect={inspect} + ip={ip} + data={ipOverviewData} + anomaliesData={anomaliesData} + loading={loading} + isLoadingAnomaliesData={isLoadingAnomaliesData} + type={type} + flowTarget={flowTarget} + refetch={refetch} + setQuery={setQuery} + startDate={from} + endDate={to} + narrowDateRange={narrowDateRange} + /> + )} + </AnomalyTableProvider> + )} + </IpOverviewQuery> + + <EuiHorizontalRule /> + + <ConditionalFlexGroup direction="column"> + <EuiFlexItem> + <NetworkTopNFlowQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={FlowTargetSourceDest.source} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + + <EuiFlexItem> + <NetworkTopNFlowQueryTable + endDate={to} + flowTarget={FlowTargetSourceDest.destination} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + </ConditionalFlexGroup> + + <EuiSpacer /> + + <ConditionalFlexGroup direction="column"> + <EuiFlexItem> + <NetworkTopCountriesQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={FlowTargetSourceDest.source} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + + <EuiFlexItem> + <NetworkTopCountriesQueryTable + endDate={to} + flowTarget={FlowTargetSourceDest.destination} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + </ConditionalFlexGroup> + + <EuiSpacer /> + + <UsersQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={flowTarget} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + /> + + <EuiSpacer /> + + <NetworkHttpQueryTable + endDate={to} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + /> + + <EuiSpacer /> + + <TlsQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={(flowTarget as unknown) as FlowTargetSourceDest} + ip={ip} + setQuery={setQuery} + skip={isInitializing} + startDate={from} + type={type} + /> + + <EuiSpacer /> + + <AnomaliesQueryTabBody + filterQuery={filterQuery} + setQuery={setQuery} + startDate={from} + endDate={to} + skip={isInitializing} + ip={ip} + type={type} + flowTarget={flowTarget} + narrowDateRange={narrowDateRange} + hideHistogramIfEmpty={true} + AnomaliesTableComponent={AnomaliesNetworkTable} + /> + </WrapperPage> + </StickyContainer> + ) : ( + <WrapperPage> + <HeaderPage border title={ip} /> + + <NetworkEmptyPage /> + </WrapperPage> + ); + }} + </WithSource> + + <SpyRoute /> + </> + ); +}; +IPDetailsComponent.displayName = 'IPDetailsComponent'; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, +}; + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/plugins/siem/public/pages/network/ip_details/types.ts new file mode 100644 index 0000000000000..11c41fc74515e --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/types.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IIndexPattern } from 'src/plugins/data/public'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NetworkType } from '../../../store/network/model'; +import { InspectQuery, Refetch } from '../../../store/inputs/model'; +import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { GlobalTimeArgs } from '../../../containers/global_time'; + +export const type = NetworkType.details; + +export type IPDetailsComponentProps = GlobalTimeArgs & { + detailName: string; + flowTarget: FlowTarget; +}; + +export interface OwnProps { + type: NetworkType; + startDate: number; + endDate: number; + filterQuery: string | ESTermQuery; + ip: string; + skip: boolean; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; + }) => void; +} + +export type NetworkComponentsQueryProps = OwnProps & { + flowTarget: FlowTarget; +}; + +export type TlsQueryTableComponentProps = OwnProps & { + flowTarget: FlowTargetSourceDest; +}; + +export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { + flowTarget: FlowTargetSourceDest; + indexPattern: IIndexPattern; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/users_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/users_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts new file mode 100644 index 0000000000000..9d15d7ee250c9 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { decodeIpv6 } from '../../../lib/helpers'; +import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; +import { networkModel } from '../../../store/network'; +import * as i18n from '../translations'; +import { NetworkRouteType } from '../navigation/types'; +import { NetworkRouteSpyState } from '../../../utils/route/types'; + +export const type = networkModel.NetworkType.details; +const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { + [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, + [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, + [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, + [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, +}; + +export const getBreadcrumbs = ( + params: NetworkRouteSpyState, + search: string[] +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: decodeIpv6(params.detailName), + href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ + !isEmpty(search[1]) ? search[1] : '' + }`, + }, + ]; + } + + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..4c4f6c06ce1e1 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { NetworkComponentQueryProps } from './types'; + +export const filterNetworkData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', + }, + }, +]; + +export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( + <AlertsView {...alertsProps} pageFilters={filterNetworkData} /> +)); + +NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx b/x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts b/x-pack/plugins/siem/public/pages/network/navigation/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts rename to x-pack/plugins/siem/public/pages/network/navigation/index.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx b/x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx diff --git a/x-pack/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/plugins/siem/public/pages/network/navigation/types.ts new file mode 100644 index 0000000000000..ee03bff99b967 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/navigation/types.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { NavTab } from '../../../components/navigation/types'; +import { FlowTargetSourceDest } from '../../../graphql/types'; +import { networkModel } from '../../../store'; +import { GlobalTimeArgs } from '../../../containers/global_time'; + +import { SetAbsoluteRangeDatePicker } from '../types'; +import { NarrowDateRange } from '../../../components/ml/types'; + +interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { + skip: boolean; + type: networkModel.NetworkType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; + narrowDateRange?: NarrowDateRange; +} + +export type NetworkComponentQueryProps = QueryTabBodyProps; + +export type IPsQueryTabBodyProps = QueryTabBodyProps & { + indexPattern: IIndexPattern; + flowTarget: FlowTargetSourceDest; +}; + +export type TlsQueryTabBodyProps = QueryTabBodyProps & { + flowTarget: FlowTargetSourceDest; + ip?: string; +}; + +export type HttpQueryTabBodyProps = QueryTabBodyProps & { + ip?: string; +}; + +export type NetworkRoutesProps = GlobalTimeArgs & { + networkPagePath: string; + type: networkModel.NetworkType; + filterQuery?: string | ESTermQuery; + indexPattern: IIndexPattern; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; +}; + +export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & + NetworkRouteType.flows & + NetworkRouteType.http & + NetworkRouteType.tls & + NetworkRouteType.alerts; + +type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & + NetworkRouteType.anomalies; + +type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; + +export type NetworkNavTab = Record<KeyNetworkNavTab, NavTab>; + +export enum NetworkRouteType { + flows = 'flows', + dns = 'dns', + anomalies = 'anomalies', + tls = 'tls', + http = 'http', + alerts = 'alerts', +} + +export type GetNetworkRoutePath = ( + pagePath: string, + capabilitiesFetched: boolean, + hasMlUserPermission: boolean +) => string; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/plugins/siem/public/pages/network/navigation/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts rename to x-pack/plugins/siem/public/pages/network/navigation/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/plugins/siem/public/pages/network/network.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx rename to x-pack/plugins/siem/public/pages/network/network.test.tsx index 797fef1586518..300cb83c4ce75 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.test.tsx @@ -11,7 +11,7 @@ import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import '../../mock/match_media'; -import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders, mockGlobalState, apolloClientObservable } from '../../mock'; import { State, createStore } from '../../store'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/plugins/siem/public/pages/network/network.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/network/network.tsx rename to x-pack/plugins/siem/public/pages/network/network.tsx index 9b1ee76e1d376..65b3142c8c074 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.tsx @@ -10,7 +10,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { esQuery } from '../../../../../../src/plugins/data/public'; import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx b/x-pack/plugins/siem/public/pages/network/network_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx rename to x-pack/plugins/siem/public/pages/network/network_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/plugins/siem/public/pages/network/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/translations.ts rename to x-pack/plugins/siem/public/pages/network/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/types.ts b/x-pack/plugins/siem/public/pages/network/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/types.ts rename to x-pack/plugins/siem/public/pages/network/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx rename to x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx new file mode 100644 index 0000000000000..a1936cf9221f8 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import { Position } from '@elastic/charts'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; +import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { HostsType } from '../../../store/hosts/model'; + +import * as i18n from '../translations'; +import { + alertsStackByOptions, + histogramConfigs, +} from '../../../components/alerts_viewer/histogram_configs'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; + +const ID = 'alertsByCategoryOverview'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.module'; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + hideHeaderChildren?: boolean; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const AlertsByCategoryComponent: React.FC<Props> = ({ + deleteQuery, + filters = NO_FILTERS, + from, + hideHeaderChildren = false, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const alertsCountViewAlertsButton = useMemo( + () => ( + <EuiButton data-test-subj="view-alerts" href={getDetectionEngineAlertUrl(urlSearch)}> + {i18n.VIEW_ALERTS} + </EuiButton> + ), + [urlSearch] + ); + + const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + legendPosition: Position.Right, + }), + [] + ); + + return ( + <MatrixHistogramContainer + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + })} + headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={from} + type={HostsType.page} + {...alertsByCategoryHistogramConfigs} + /> + ); +}; + +AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; + +export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.test.tsx rename to x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx new file mode 100644 index 0000000000000..f242b0d84d7c1 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { OverviewHost } from '../../../components/page/overview/overview_host'; +import { OverviewNetwork } from '../../../components/page/overview/overview_network'; +import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; + +const HorizontalSpacer = styled(EuiFlexItem)` + width: 24px; +`; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const EventCountsComponent: React.FC<Props> = ({ + filters = NO_FILTERS, + from, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + const kibana = useKibana(); + + return ( + <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={true}> + <OverviewHost + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterHostData], + })} + startDate={from} + setQuery={setQuery} + /> + </EuiFlexItem> + + <HorizontalSpacer grow={false} /> + + <EuiFlexItem grow={true}> + <OverviewNetwork + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterNetworkData], + })} + startDate={from} + setQuery={setQuery} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx rename to x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx new file mode 100644 index 0000000000000..77d6da7a7efc4 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Position } from '@elastic/charts'; +import { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; +import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { + MatrixHisrogramConfigs, + MatrixHistogramOption, +} from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; +import { eventsStackByOptions } from '../../hosts/navigation'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { HostsTableType, HostsType } from '../../../store/hosts/model'; +import { InputsModelId } from '../../../store/inputs/constants'; + +import * as i18n from '../translations'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.dataset'; + +const ID = 'eventsByDatasetOverview'; + +interface Props { + combinedQueries?: string; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + showSpacer?: boolean; + to: number; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const EventsByDatasetComponent: React.FC<Props> = ({ + combinedQueries, + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + indexPattern, + indexToAdd, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePickerTarget, + setQuery, + showSpacer = true, + to, +}) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, [deleteQuery, uniqueQueryId]); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + + const eventsCountViewEventsButton = useMemo( + () => ( + <EuiButton href={getTabsOnHostsUrl(HostsTableType.events, urlSearch)}> + {i18n.VIEW_EVENTS} + </EuiButton> + ), + [urlSearch] + ); + + const filterQuery = useMemo( + () => + combinedQueries == null + ? convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }) + : combinedQueries, + [combinedQueries, kibana, indexPattern, query, filters] + ); + + const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + stackByOptions: + onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, + defaultStackByOption: + onlyField != null + ? getHistogramOption(onlyField) + : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + legendPosition: Position.Right, + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + titleSize: onlyField == null ? 'm' : 's', + }), + [onlyField, defaultNumberFormat] + ); + + const headerContent = useMemo(() => { + if (onlyField == null || headerChildren != null) { + return ( + <> + {headerChildren} + {onlyField == null && eventsCountViewEventsButton} + </> + ); + } else { + return null; + } + }, [onlyField, headerChildren, eventsCountViewEventsButton]); + + return ( + <MatrixHistogramContainer + endDate={to} + filterQuery={filterQuery} + headerChildren={headerContent} + id={uniqueQueryId} + indexToAdd={indexToAdd} + setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} + setQuery={setQuery} + showSpacer={showSpacer} + sourceId="default" + startDate={from} + type={HostsType.page} + {...eventsByDatasetHistogramConfigs} + title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} + /> + ); +}; + +EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; + +export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx b/x-pack/plugins/siem/public/pages/overview/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/index.tsx rename to x-pack/plugins/siem/public/pages/overview/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/plugins/siem/public/pages/overview/overview.test.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx rename to x-pack/plugins/siem/public/pages/overview/overview.test.tsx index b20cd84295566..c129258fa2e87 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/plugins/siem/public/pages/overview/overview.test.tsx @@ -15,14 +15,7 @@ import { TestProviders } from '../../mock'; import { mocksSource } from '../../containers/source/mock'; import { Overview } from './index'; -jest.mock('ui/chrome', () => ({ - getKibanaVersion: () => { - return 'v8.0.0'; - }, - breadcrumbs: { - set: jest.fn(), - }, -})); +jest.mock('../../lib/kibana'); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/plugins/siem/public/pages/overview/overview.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx rename to x-pack/plugins/siem/public/pages/overview/overview.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx b/x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx rename to x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx b/x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx rename to x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx rename to x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx index 4d4d96803cd65..c972fd83cc88f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx +++ b/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx @@ -8,10 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { - ENABLE_NEWS_FEED_SETTING, - NEWS_FEED_URL_SETTING, -} from '../../../../../../../plugins/siem/common/constants'; +import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; import { Filters as RecentCasesFilters } from '../../../components/recent_cases/filters'; import { Filters as RecentTimelinesFilters } from '../../../components/recent_timelines/filters'; import { StatefulRecentCases } from '../../../components/recent_cases'; diff --git a/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx new file mode 100644 index 0000000000000..6357d93d7ba71 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; + +import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; +import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config'; +import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index'; +import { SetAbsoluteRangeDatePicker } from '../../network/types'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { InputsModelId } from '../../../store/inputs/constants'; +import * as i18n from '../translations'; + +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; +const NO_FILTERS: Filter[] = []; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const SignalsByCategoryComponent: React.FC<Props> = ({ + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget = 'global', + setQuery, + to, +}) => { + const { signalIndexName } = useSignalIndex(); + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const defaultStackByOption = + signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; + + return ( + <SignalsHistogramPanel + deleteQuery={deleteQuery} + defaultStackByOption={defaultStackByOption} + filters={filters} + from={from} + headerChildren={headerChildren} + onlyField={onlyField} + query={query} + signalIndexName={signalIndexName} + setQuery={setQuery} + showTotalSignalsCount={true} + showLinkToSignals={onlyField == null ? true : false} + stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} + legendPosition={'right'} + to={to} + title={i18n.SIGNAL_COUNT} + updateDateRange={updateDateRangeCallback} + /> + ); +}; + +SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; + +export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx b/x-pack/plugins/siem/public/pages/overview/summary.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx rename to x-pack/plugins/siem/public/pages/overview/summary.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts b/x-pack/plugins/siem/public/pages/overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/translations.ts rename to x-pack/plugins/siem/public/pages/overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx rename to x-pack/plugins/siem/public/pages/timelines/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx rename to x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx rename to x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx diff --git a/x-pack/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 723d164ad2cdd..304474bbff2c5 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 0000000000000..e54c96364ba6b --- /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 0000000000000..472006a9e55b1 --- /dev/null +++ b/x-pack/plugins/siem/public/shared_imports.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FieldValidateResponse, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + useForm, + ValidationFunc, + VALIDATION_TYPES, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field, SelectField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/store/actions.ts b/x-pack/plugins/siem/public/store/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/actions.ts rename to x-pack/plugins/siem/public/store/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/actions.ts b/x-pack/plugins/siem/public/store/app/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/actions.ts rename to x-pack/plugins/siem/public/store/app/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/index.ts b/x-pack/plugins/siem/public/store/app/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/index.ts rename to x-pack/plugins/siem/public/store/app/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/model.ts b/x-pack/plugins/siem/public/store/app/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/model.ts rename to x-pack/plugins/siem/public/store/app/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/reducer.ts b/x-pack/plugins/siem/public/store/app/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/reducer.ts rename to x-pack/plugins/siem/public/store/app/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/selectors.ts b/x-pack/plugins/siem/public/store/app/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/selectors.ts rename to x-pack/plugins/siem/public/store/app/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/constants.ts b/x-pack/plugins/siem/public/store/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/constants.ts rename to x-pack/plugins/siem/public/store/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/actions.ts b/x-pack/plugins/siem/public/store/drag_and_drop/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/actions.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/index.ts b/x-pack/plugins/siem/public/store/drag_and_drop/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/index.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/model.ts b/x-pack/plugins/siem/public/store/drag_and_drop/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/model.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.test.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.test.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/selectors.ts b/x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/selectors.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/epic.ts b/x-pack/plugins/siem/public/store/epic.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/epic.ts rename to x-pack/plugins/siem/public/store/epic.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts b/x-pack/plugins/siem/public/store/hosts/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/actions.ts rename to x-pack/plugins/siem/public/store/hosts/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts b/x-pack/plugins/siem/public/store/hosts/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts rename to x-pack/plugins/siem/public/store/hosts/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts b/x-pack/plugins/siem/public/store/hosts/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts rename to x-pack/plugins/siem/public/store/hosts/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/index.ts b/x-pack/plugins/siem/public/store/hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/index.ts rename to x-pack/plugins/siem/public/store/hosts/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/plugins/siem/public/store/hosts/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/model.ts rename to x-pack/plugins/siem/public/store/hosts/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/plugins/siem/public/store/hosts/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts rename to x-pack/plugins/siem/public/store/hosts/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts b/x-pack/plugins/siem/public/store/hosts/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts rename to x-pack/plugins/siem/public/store/hosts/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/index.ts b/x-pack/plugins/siem/public/store/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/index.ts rename to x-pack/plugins/siem/public/store/index.ts diff --git a/x-pack/plugins/siem/public/store/inputs/actions.ts b/x-pack/plugins/siem/public/store/inputs/actions.ts new file mode 100644 index 0000000000000..04cdf5246de2c --- /dev/null +++ b/x-pack/plugins/siem/public/store/inputs/actions.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { InspectQuery, Refetch, RefetchKql } from './model'; +import { InputsModelId } from './constants'; +import { Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); + +export const setAbsoluteRangeDatePicker = actionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); + +export const setTimelineRangeDatePicker = actionCreator<{ + from: number; + to: number; +}>('SET_TIMELINE_RANGE_DATE_PICKER'); + +export const setRelativeRangeDatePicker = actionCreator<{ + id: InputsModelId; + fromStr: string; + toStr: string; + from: number; + to: number; +}>('SET_RELATIVE_RANGE_DATE_PICKER'); + +export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); + +export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); + +export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); + +export const setQuery = actionCreator<{ + inputId: InputsModelId; + id: string; + loading: boolean; + refetch: Refetch | RefetchKql; + inspect: InspectQuery | null; +}>('SET_QUERY'); + +export const deleteOneQuery = actionCreator<{ + inputId: InputsModelId; + id: string; +}>('DELETE_QUERY'); + +export const setInspectionParameter = actionCreator<{ + id: string; + inputId: InputsModelId; + isInspected: boolean; + selectedInspectIndex: number; +}>('SET_INSPECTION_PARAMETER'); + +export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); + +export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( + 'TOGGLE_TIMELINE_LINK_TO' +); + +export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); +export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); + +export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); +export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); + +export const setFilterQuery = actionCreator<{ + id: InputsModelId; + query: string | { [key: string]: unknown }; + language: string; +}>('SET_FILTER_QUERY'); + +export const setSavedQuery = actionCreator<{ + id: InputsModelId; + savedQuery: SavedQuery | undefined; +}>('SET_SAVED_QUERY'); + +export const setSearchBarFilter = actionCreator<{ + id: InputsModelId; + filters: Filter[]; +}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/constants.ts b/x-pack/plugins/siem/public/store/inputs/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/constants.ts rename to x-pack/plugins/siem/public/store/inputs/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/helpers.test.ts b/x-pack/plugins/siem/public/store/inputs/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/helpers.test.ts rename to x-pack/plugins/siem/public/store/inputs/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/helpers.ts b/x-pack/plugins/siem/public/store/inputs/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/helpers.ts rename to x-pack/plugins/siem/public/store/inputs/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/index.ts b/x-pack/plugins/siem/public/store/inputs/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/index.ts rename to x-pack/plugins/siem/public/store/inputs/index.ts diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts new file mode 100644 index 0000000000000..3e6be6ce859e5 --- /dev/null +++ b/x-pack/plugins/siem/public/store/inputs/model.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { InputsModelId } from './constants'; +import { CONSTANTS } from '../../components/url_state/constants'; +import { Query, Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; + +export interface AbsoluteTimeRange { + kind: 'absolute'; + fromStr: undefined; + toStr: undefined; + from: number; + to: number; +} + +export interface RelativeTimeRange { + kind: 'relative'; + fromStr: string; + toStr: string; + from: number; + to: number; +} + +export const isRelativeTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; + +export const isAbsoluteTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; + +export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; + +export type URLTimeRange = Omit<TimeRange, 'from' | 'to'> & { + from: string | TimeRange['from']; + to: string | TimeRange['to']; +}; + +export interface Policy { + kind: 'manual' | 'interval'; + duration: number; // in ms +} + +interface InspectVariables { + inspect: boolean; +} +export type RefetchWithParams = ({ inspect }: InspectVariables) => void; +export type RefetchKql = (dispatch: Dispatch) => boolean; +export type Refetch = () => void; + +export interface InspectQuery { + dsl: string[]; + response: string[]; +} + +export interface GlobalGenericQuery { + inspect: InspectQuery | null; + isInspected: boolean; + loading: boolean; + selectedInspectIndex: number; +} + +export interface GlobalGraphqlQuery extends GlobalGenericQuery { + id: string; + refetch: null | Refetch | RefetchWithParams; +} +export interface GlobalKqlQuery extends GlobalGenericQuery { + id: 'kql'; + refetch: RefetchKql; +} + +export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; + +export interface InputsRange { + timerange: TimeRange; + policy: Policy; + queries: GlobalQuery[]; + linkTo: InputsModelId[]; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; +} + +export interface LinkTo { + linkTo: InputsModelId[]; +} + +export interface InputsModel { + global: InputsRange; + timeline: InputsRange; +} +export interface UrlInputsModelInputs { + linkTo: InputsModelId[]; + [CONSTANTS.timerange]: TimeRange; +} +export interface UrlInputsModel { + global: UrlInputsModelInputs; + timeline: UrlInputsModelInputs; +} diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts b/x-pack/plugins/siem/public/store/inputs/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts rename to x-pack/plugins/siem/public/store/inputs/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts b/x-pack/plugins/siem/public/store/inputs/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts rename to x-pack/plugins/siem/public/store/inputs/selectors.ts diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts new file mode 100644 index 0000000000000..686dc096e61b0 --- /dev/null +++ b/x-pack/plugins/siem/public/store/model.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { appModel } from './app'; +export { dragAndDropModel } from './drag_and_drop'; +export { hostsModel } from './hosts'; +export { inputsModel } from './inputs'; +export { networkModel } from './network'; +export * from './types'; diff --git a/x-pack/legacy/plugins/siem/public/store/network/actions.ts b/x-pack/plugins/siem/public/store/network/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/actions.ts rename to x-pack/plugins/siem/public/store/network/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/plugins/siem/public/store/network/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts rename to x-pack/plugins/siem/public/store/network/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts b/x-pack/plugins/siem/public/store/network/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/helpers.ts rename to x-pack/plugins/siem/public/store/network/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/index.ts b/x-pack/plugins/siem/public/store/network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/index.ts rename to x-pack/plugins/siem/public/store/network/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/plugins/siem/public/store/network/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/model.ts rename to x-pack/plugins/siem/public/store/network/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/plugins/siem/public/store/network/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/reducer.ts rename to x-pack/plugins/siem/public/store/network/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts b/x-pack/plugins/siem/public/store/network/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/selectors.ts rename to x-pack/plugins/siem/public/store/network/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/reducer.ts b/x-pack/plugins/siem/public/store/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/reducer.ts rename to x-pack/plugins/siem/public/store/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/selectors.ts b/x-pack/plugins/siem/public/store/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/selectors.ts rename to x-pack/plugins/siem/public/store/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/store.ts b/x-pack/plugins/siem/public/store/store.ts similarity index 96% rename from x-pack/legacy/plugins/siem/public/store/store.ts rename to x-pack/plugins/siem/public/store/store.ts index d3559e7a7adde..2af0f87b4494d 100644 --- a/x-pack/legacy/plugins/siem/public/store/store.ts +++ b/x-pack/plugins/siem/public/store/store.ts @@ -32,6 +32,7 @@ export const createStore = ( const middlewareDependencies = { apolloClient$: apolloClient, + selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, selectNotesByIdSelector: appSelectors.selectNotesByIdSelector, timelineByIdSelector: timelineSelectors.timelineByIdSelector, timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector, diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts new file mode 100644 index 0000000000000..12155decf40d4 --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { Sort } from '../../components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, +} from '../../components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../types'; + +import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; +import { TimelineNonEcsData } from '../../graphql/types'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); + +export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); + +export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); + +export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( + 'ADD_NOTE_TO_EVENT' +); + +export const upsertColumn = actionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>('UPSERT_COLUMN'); + +export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); + +export const applyDeltaToWidth = actionCreator<{ + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; +}>('APPLY_DELTA_TO_WIDTH'); + +export const applyDeltaToColumnWidth = actionCreator<{ + id: string; + columnId: string; + delta: number; +}>('APPLY_DELTA_TO_COLUMN_WIDTH'); + +export const createTimeline = actionCreator<{ + id: string; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + columns: ColumnHeaderOptions[]; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; +}>('CREATE_TIMELINE'); + +export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); + +export const removeColumn = actionCreator<{ + id: string; + columnId: string; +}>('REMOVE_COLUMN'); + +export const removeProvider = actionCreator<{ + id: string; + providerId: string; + andProviderId?: string; +}>('REMOVE_PROVIDER'); + +export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); + +export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); + +export const updateTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('UPDATE_TIMELINE'); + +export const addTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('ADD_TIMELINE'); + +export const startTimelineSaving = actionCreator<{ + id: string; +}>('START_TIMELINE_SAVING'); + +export const endTimelineSaving = actionCreator<{ + id: string; +}>('END_TIMELINE_SAVING'); + +export const updateIsLoading = actionCreator<{ + id: string; + isLoading: boolean; +}>('UPDATE_LOADING'); + +export const updateColumns = actionCreator<{ + id: string; + columns: ColumnHeaderOptions[]; +}>('UPDATE_COLUMNS'); + +export const updateDataProviderEnabled = actionCreator<{ + id: string; + enabled: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_ENABLED'); + +export const updateDataProviderExcluded = actionCreator<{ + id: string; + excluded: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_EXCLUDED'); + +export const dataProviderEdited = actionCreator<{ + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + value: string | number; +}>('DATA_PROVIDER_EDITED'); + +export const updateDataProviderKqlQuery = actionCreator<{ + id: string; + kqlQuery: string; + providerId: string; +}>('PROVIDER_EDIT_KQL_QUERY'); + +export const updateHighlightedDropAndProviderId = actionCreator<{ + id: string; + providerId: string; +}>('UPDATE_DROP_AND_PROVIDER'); + +export const updateDescription = actionCreator<{ id: string; description: string }>( + 'UPDATE_DESCRIPTION' +); + +export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); + +export const setKqlFilterQueryDraft = actionCreator<{ + id: string; + filterQueryDraft: KueryFilterQuery; +}>('SET_KQL_FILTER_QUERY_DRAFT'); + +export const applyKqlFilterQuery = actionCreator<{ + id: string; + filterQuery: SerializedFilterQuery; +}>('APPLY_KQL_FILTER_QUERY'); + +export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( + 'UPDATE_IS_FAVORITE' +); + +export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); + +export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( + 'UPDATE_ITEMS_PER_PAGE' +); + +export const updateItemsPerPageOptions = actionCreator<{ + id: string; + itemsPerPageOptions: number[]; +}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); + +export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); + +export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( + 'UPDATE_PAGE_INDEX' +); + +export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( + 'UPDATE_PROVIDERS' +); + +export const updateRange = actionCreator<{ id: string; start: number; end: number }>( + 'UPDATE_RANGE' +); + +export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); + +export const updateAutoSaveMsg = actionCreator<{ + timelineId: string | null; + newTimelineModel: TimelineModel | null; +}>('UPDATE_AUTO_SAVE'); + +export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); + +export const setSavedQueryId = actionCreator<{ + id: string; + savedQueryId: string | null; +}>('SET_TIMELINE_SAVED_QUERY'); + +export const setFilters = actionCreator<{ + id: string; + filters: Filter[]; +}>('SET_TIMELINE_FILTERS'); + +export const setSelected = actionCreator<{ + id: string; + eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + isSelected: boolean; + isSelectAllChecked: boolean; +}>('SET_TIMELINE_SELECTED'); + +export const clearSelected = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_SELECTED'); + +export const setEventsLoading = actionCreator<{ + id: string; + eventIds: string[]; + isLoading: boolean; +}>('SET_TIMELINE_EVENTS_LOADING'); + +export const clearEventsLoading = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_LOADING'); + +export const setEventsDeleted = actionCreator<{ + id: string; + eventIds: string[]; + isDeleted: boolean; +}>('SET_TIMELINE_EVENTS_DELETED'); + +export const clearEventsDeleted = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_DELETED'); + +export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( + 'UPDATE_EVENT_TYPE' +); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/store/timeline/defaults.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts rename to x-pack/plugins/siem/public/store/timeline/defaults.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/plugins/siem/public/store/timeline/epic.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts rename to x-pack/plugins/siem/public/store/timeline/epic.test.ts index 13d30825a169c..bb6babcba41ae 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.test.ts @@ -8,7 +8,7 @@ import { TimelineModel } from './model'; import { Direction } from '../../graphql/types'; import { convertTimelineAsInput } from './epic'; -import { Filter, esFilters } from '../../../../../../../src/plugins/data/public'; +import { Filter, esFilters } from '../../../../../../src/plugins/data/public'; describe('Epic Timeline', () => { describe('#convertTimelineAsInput ', () => { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts similarity index 85% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic.ts rename to x-pack/plugins/siem/public/store/timeline/epic.ts index e6acff8736492..6812d8d8aa672 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.ts @@ -28,18 +28,12 @@ import { takeUntil, } from 'rxjs/operators'; -import { esFilters, Filter, MatchAllFilter } from '../../../../../../../src/plugins/data/public'; -import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query'; -import { - PersistTimelineMutation, - TimelineInput, - ResponseTimeline, - TimelineResult, -} from '../../graphql/types'; +import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public'; +import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types'; import { AppApolloClient } from '../../lib/lib'; import { addError } from '../app/actions'; import { NotesById } from '../app/model'; -import { TimeRange } from '../inputs/model'; +import { inputsModel } from '../inputs'; import { applyKqlFilterQuery, @@ -75,13 +69,15 @@ import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_p import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; import { isNotNull } from './helpers'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { persistTimeline } from '../../containers/timeline/api'; +import { ALL_TIMELINE_QUERY_ID } from '../../containers/timeline/all'; interface TimelineEpicDependencies<State> { timelineByIdSelector: (state: State) => TimelineById; - timelineTimeRangeSelector: (state: State) => TimeRange; + timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; + selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable<AppApolloClient>; } @@ -119,10 +115,24 @@ export const createTimelineEpic = <State>(): Epic< > => ( action$, state$, - { selectNotesByIdSelector, timelineByIdSelector, timelineTimeRangeSelector, apolloClient$ } + { + selectAllTimelineQuery, + selectNotesByIdSelector, + timelineByIdSelector, + timelineTimeRangeSelector, + apolloClient$, + } ) => { const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); + const allTimelineQuery$ = state$.pipe( + map(state => { + const getQuery = selectAllTimelineQuery(); + return getQuery(state, ALL_TIMELINE_QUERY_ID); + }), + filter(isNotNull) + ); + const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull)); const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull)); @@ -168,33 +178,52 @@ export const createTimelineEpic = <State>(): Epic< const version = myEpicTimelineId.getTimelineVersion(); if (timelineNoteActionsType.includes(action.type)) { - return epicPersistNote(apolloClient, action, timeline, notes, action$, timeline$, notes$); + return epicPersistNote( + apolloClient, + action, + timeline, + notes, + action$, + timeline$, + notes$, + allTimelineQuery$ + ); } else if (timelinePinnedEventActionsType.includes(action.type)) { - return epicPersistPinnedEvent(apolloClient, action, timeline, action$, timeline$); + return epicPersistPinnedEvent( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineFavoriteActionsType.includes(action.type)) { - return epicPersistTimelineFavorite(apolloClient, action, timeline, action$, timeline$); + return epicPersistTimelineFavorite( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineActionsType.includes(action.type)) { return from( - apolloClient.mutate< - PersistTimelineMutation.Mutation, - PersistTimelineMutation.Variables - >({ - mutation: persistTimelineMutation, - fetchPolicy: 'no-cache', - variables: { - timelineId, - version, - timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), - }, - refetchQueries, + persistTimeline({ + timelineId, + version, + timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: ResponseTimeline = get('data.persistTimeline', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response.code === 409 ? updateAutoSaveMsg({ @@ -261,7 +290,7 @@ const timelineInput: TimelineInput = { export const convertTimelineAsInput = ( timeline: TimelineModel, - timelineTimeRange: TimeRange + timelineTimeRange: inputsModel.TimeRange ): TimelineInput => Object.keys(timelineInput).reduce<TimelineInput>((acc, key) => { if (has(key, timeline)) { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts b/x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts rename to x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts rename to x-pack/plugins/siem/public/store/timeline/epic_favorite.ts index 4d1b73aa70a6e..6a1dadb8a59f5 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts @@ -26,6 +26,7 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { inputsModel } from '../inputs'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; @@ -34,7 +35,8 @@ export const epicPersistTimelineFavorite = ( action: ActionTimeline, timeline: TimelineById, action$: Observable<Action>, - timeline$: Observable<TimelineById> + timeline$: Observable<TimelineById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -50,12 +52,16 @@ export const epicPersistTimelineFavorite = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimelines]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimelines, allTimelineQuery]) => { const savedTimeline = recentTimelines[action.payload.id]; const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, updateTimeline({ diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/plugins/siem/public/store/timeline/epic_note.ts similarity index 92% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts rename to x-pack/plugins/siem/public/store/timeline/epic_note.ts index e5a712fe2c666..3722a6ad8036c 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_note.ts @@ -16,6 +16,7 @@ import { persistTimelineNoteMutation } from '../../containers/timeline/notes/per import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types'; import { updateNote, addError } from '../app/actions'; import { NotesById } from '../app/model'; +import { inputsModel } from '../inputs'; import { addNote, @@ -39,7 +40,8 @@ export const epicPersistNote = ( notes: NotesById, action$: Observable<Action>, timeline$: Observable<TimelineById>, - notes$: Observable<NotesById> + notes$: Observable<NotesById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -61,12 +63,16 @@ export const epicPersistNote = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$, notes$), - mergeMap(([result, recentTimeline, recentNotes]) => { + withLatestFrom(timeline$, notes$, allTimelineQuery$), + mergeMap(([result, recentTimeline, recentNotes, allTimelineQuery]) => { const noteIdRedux = action.payload.noteId; const response: ResponseNote = get('data.persistNote', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, recentTimeline[action.payload.id].savedObjectId == null diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts similarity index 93% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts rename to x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts index 2260999a91e7b..a1281250ba72a 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -15,6 +15,8 @@ import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/ope import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query'; import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types'; import { addError } from '../app/actions'; +import { inputsModel } from '../inputs'; + import { pinEvent, endTimelineSaving, @@ -35,7 +37,8 @@ export const epicPersistPinnedEvent = ( action: ActionTimeline, timeline: TimelineById, action$: Observable<Action>, - timeline$: Observable<TimelineById> + timeline$: Observable<TimelineById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -57,12 +60,16 @@ export const epicPersistPinnedEvent = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result); const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response != null ? updateTimeline({ diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts new file mode 100644 index 0000000000000..19de49918d100 --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/helpers.ts @@ -0,0 +1,1324 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; +import { Sort } from '../../components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, + QueryMatch, +} from '../../components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../model'; + +import { timelineDefaults } from './defaults'; +import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; +import { TimelineById, TimelineState } from './types'; +import { TimelineNonEcsData } from '../../graphql/types'; + +const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference + +export const isNotNull = <T>(value: T | null): value is T => value !== null; + +export const initialTimelineState: TimelineState = { + timelineById: EMPTY_TIMELINE_BY_ID, + autoSavedWarningMsg: { + timelineId: null, + newTimelineModel: null, + }, + showCallOutUnauthorizedMsg: false, +}; + +interface AddTimelineHistoryParams { + id: string; + historyId: string; + timelineById: TimelineById; +} + +export const addTimelineHistory = ({ + id, + historyId, + timelineById, +}: AddTimelineHistoryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + historyIds: uniq([...timeline.historyIds, historyId]), + }, + }; +}; + +interface AddTimelineNoteParams { + id: string; + noteId: string; + timelineById: TimelineById; +} + +export const addTimelineNote = ({ + id, + noteId, + timelineById, +}: AddTimelineNoteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + noteIds: [...timeline.noteIds, noteId], + }, + }; +}; + +interface AddTimelineNoteToEventParams { + id: string; + noteId: string; + eventId: string; + timelineById: TimelineById; +} + +export const addTimelineNoteToEvent = ({ + id, + noteId, + eventId, + timelineById, +}: AddTimelineNoteToEventParams): TimelineById => { + const timeline = timelineById[id]; + const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); + + return { + ...timelineById, + [id]: { + ...timeline, + eventIdToNoteIds: { + ...timeline.eventIdToNoteIds, + ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, + }, + }, + }; +}; + +interface AddTimelineParams { + id: string; + timeline: TimelineModel; + timelineById: TimelineById; +} + +/** + * Add a saved object timeline to the store + * and default the value to what need to be if values are null + */ +export const addTimelineToStore = ({ + id, + timeline, + timelineById, +}: AddTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + ...timeline, + isLoading: timelineById[id].isLoading, + }, +}); + +interface AddNewTimelineParams { + columns: ColumnHeaderOptions[]; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + id: string; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; + timelineById: TimelineById; +} + +/** Adds a new `Timeline` to the provided collection of `TimelineById` */ +export const addNewTimeline = ({ + columns, + dataProviders = [], + dateRange = { start: 0, end: 0 }, + filters = timelineDefaults.filters, + id, + itemsPerPage = timelineDefaults.itemsPerPage, + kqlQuery = { filterQuery: null, filterQueryDraft: null }, + sort = timelineDefaults.sort, + show = false, + showCheckboxes = false, + showRowRenderers = true, + timelineById, +}: AddNewTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + id, + ...timelineDefaults, + columns, + dataProviders, + dateRange, + filters, + itemsPerPage, + kqlQuery, + sort, + show, + savedObjectId: null, + version: null, + isSaving: false, + isLoading: false, + showCheckboxes, + showRowRenderers, + }, +}); + +interface PinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const pinTimelineEvent = ({ + id, + eventId, + timelineById, +}: PinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: { + ...timeline.pinnedEventIds, + ...{ [eventId]: true }, + }, + }, + }; +}; + +interface UpdateShowTimelineProps { + id: string; + show: boolean; + timelineById: TimelineById; +} + +export const updateTimelineShowTimeline = ({ + id, + show, + timelineById, +}: UpdateShowTimelineProps): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + show, + }, + }; +}; + +interface ApplyDeltaToCurrentWidthParams { + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; + timelineById: TimelineById; +} + +export const applyDeltaToCurrentWidth = ({ + id, + delta, + bodyClientWidthPixels, + minWidthPixels, + maxWidthPercent, + timelineById, +}: ApplyDeltaToCurrentWidthParams): TimelineById => { + const timeline = timelineById[id]; + + const requestedWidth = timeline.width + delta * -1; // raw change in width + const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; + const clampedWidth = Math.min(requestedWidth, maxWidthPixels); + const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min + + return { + ...timelineById, + [id]: { + ...timeline, + width, + }, + }; +}; + +const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { + if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { + return true; + } + return false; +}; + +const addAndToProviderInTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( + p => p.id === timeline.highlightedDropAndProviderId + ); + const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; + const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); + const { and, ...andProvider } = provider; + + if ( + isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || + (alreadyExistsAndProviderIndex === -1 && + newProvider.and.filter(itemAndProvider => + isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) + ).length > 0) + ) { + return timelineById; + } + + const dataProviders = [ + ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), + { + ...timeline.dataProviders[alreadyExistsProviderIndex], + and: + alreadyExistsAndProviderIndex > -1 + ? [ + ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), + andProvider, + ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), + ] + : [...newProvider.and, andProvider], + }, + ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +const addProviderToTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); + + if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { + provider.id = `${provider.id}-${ + timeline.dataProviders.filter(p => p.id === provider.id).length + }`; + } + + const dataProviders = + alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) + ? [ + ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), + provider, + ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), + ] + : [...timeline.dataProviders, provider]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +interface AddTimelineColumnParams { + column: ColumnHeaderOptions; + id: string; + index: number; + timelineById: TimelineById; +} + +/** + * Adds or updates a column. When updating a column, it will be moved to the + * new index + */ +export const upsertTimelineColumn = ({ + column, + id, + index, + timelineById, +}: AddTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); + + if (alreadyExistsAtIndex !== -1) { + // remove the existing entry and add the new one at the specified index + const reordered = timeline.columns.filter(c => c.id !== column.id); + reordered.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns: reordered, + }, + }; + } + + // add the new entry at the specified index + const columns = [...timeline.columns]; + columns.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface RemoveTimelineColumnParams { + id: string; + columnId: string; + timelineById: TimelineById; +} + +export const removeTimelineColumn = ({ + id, + columnId, + timelineById, +}: RemoveTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + + const columns = timeline.columns.filter(c => c.id !== columnId); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface ApplyDeltaToTimelineColumnWidth { + id: string; + columnId: string; + delta: number; + timelineById: TimelineById; +} + +export const applyDeltaToTimelineColumnWidth = ({ + id, + columnId, + delta, + timelineById, +}: ApplyDeltaToTimelineColumnWidth): TimelineById => { + const timeline = timelineById[id]; + + const columnIndex = timeline.columns.findIndex(c => c.id === columnId); + if (columnIndex === -1) { + // the column was not found + return { + ...timelineById, + [id]: { + ...timeline, + }, + }; + } + const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); + const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width + const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min + + const columnWithNewWidth = { + ...timeline.columns[columnIndex], + width, + }; + + const columns = [ + ...timeline.columns.slice(0, columnIndex), + columnWithNewWidth, + ...timeline.columns.slice(columnIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface AddTimelineProviderParams { + id: string; + provider: DataProvider; + timelineById: TimelineById; +} + +export const addTimelineProvider = ({ + id, + provider, + timelineById, +}: AddTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + if (timeline.highlightedDropAndProviderId !== '') { + return addAndToProviderInTimeline(id, provider, timeline, timelineById); + } else { + return addProviderToTimeline(id, provider, timeline, timelineById); + } +}; + +interface ApplyKqlFilterQueryDraftParams { + id: string; + filterQuery: SerializedFilterQuery; + timelineById: TimelineById; +} + +export const applyKqlFilterQueryDraft = ({ + id, + filterQuery, + timelineById, +}: ApplyKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQuery, + }, + }, + }; +}; + +interface UpdateTimelineKqlModeParams { + id: string; + kqlMode: KqlMode; + timelineById: TimelineById; +} + +export const updateTimelineKqlMode = ({ + id, + kqlMode, + timelineById, +}: UpdateTimelineKqlModeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlMode, + }, + }; +}; + +interface UpdateKqlFilterQueryDraftParams { + id: string; + filterQueryDraft: KueryFilterQuery; + timelineById: TimelineById; +} + +export const updateKqlFilterQueryDraft = ({ + id, + filterQueryDraft, + timelineById, +}: UpdateKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQueryDraft, + }, + }, + }; +}; + +interface UpdateTimelineColumnsParams { + id: string; + columns: ColumnHeaderOptions[]; + timelineById: TimelineById; +} + +export const updateTimelineColumns = ({ + id, + columns, + timelineById, +}: UpdateTimelineColumnsParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface UpdateTimelineDescriptionParams { + id: string; + description: string; + timelineById: TimelineById; +} + +export const updateTimelineDescription = ({ + id, + description, + timelineById, +}: UpdateTimelineDescriptionParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), + }, + }; +}; + +interface UpdateTimelineTitleParams { + id: string; + title: string; + timelineById: TimelineById; +} + +export const updateTimelineTitle = ({ + id, + title, + timelineById, +}: UpdateTimelineTitleParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), + }, + }; +}; + +interface UpdateTimelineEventTypeParams { + id: string; + eventType: EventType; + timelineById: TimelineById; +} + +export const updateTimelineEventType = ({ + id, + eventType, + timelineById, +}: UpdateTimelineEventTypeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + eventType, + }, + }; +}; + +interface UpdateTimelineIsFavoriteParams { + id: string; + isFavorite: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsFavorite = ({ + id, + isFavorite, + timelineById, +}: UpdateTimelineIsFavoriteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isFavorite, + }, + }; +}; + +interface UpdateTimelineIsLiveParams { + id: string; + isLive: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsLive = ({ + id, + isLive, + timelineById, +}: UpdateTimelineIsLiveParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isLive, + }, + }; +}; + +interface UpdateTimelineProvidersParams { + id: string; + providers: DataProvider[]; + timelineById: TimelineById; +} + +export const updateTimelineProviders = ({ + id, + providers, + timelineById, +}: UpdateTimelineProvidersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: providers, + }, + }; +}; + +interface UpdateTimelineRangeParams { + id: string; + start: number; + end: number; + timelineById: TimelineById; +} + +export const updateTimelineRange = ({ + id, + start, + end, + timelineById, +}: UpdateTimelineRangeParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dateRange: { + start, + end, + }, + }, + }; +}; + +interface UpdateTimelineSortParams { + id: string; + sort: Sort; + timelineById: TimelineById; +} + +export const updateTimelineSort = ({ + id, + sort, + timelineById, +}: UpdateTimelineSortParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + sort, + }, + }; +}; + +const updateEnabledAndProvider = ( + andProviderId: string, + enabled: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider + ), + } + : provider + ); + +const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + enabled, + } + : provider + ); + +interface UpdateTimelineProviderEnabledParams { + id: string; + providerId: string; + enabled: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderEnabled = ({ + id, + providerId, + enabled, + timelineById, + andProviderId, +}: UpdateTimelineProviderEnabledParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) + : updateEnabledProvider(enabled, providerId, timeline), + }, + }; +}; + +const updateExcludedAndProvider = ( + andProviderId: string, + excluded: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider + ), + } + : provider + ); + +const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + } + : provider + ); + +interface UpdateTimelineProviderExcludedParams { + id: string; + providerId: string; + excluded: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderExcluded = ({ + id, + providerId, + excluded, + timelineById, + andProviderId, +}: UpdateTimelineProviderExcludedParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) + : updateExcludedProvider(excluded, providerId, timeline), + }, + }; +}; + +const updateProviderProperties = ({ + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + queryMatch: { + ...provider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : provider + ); + +const updateAndProviderProperties = ({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + andProviderId: string; + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId + ? { + ...andProvider, + excluded, + queryMatch: { + ...andProvider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : andProvider + ), + } + : provider + ); + +interface UpdateTimelineProviderEditPropertiesParams { + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + timelineById: TimelineById; + value: string | number; +} + +export const updateTimelineProviderProperties = ({ + andProviderId, + excluded, + field, + id, + operator, + providerId, + timelineById, + value, +}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateAndProviderProperties({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, + }) + : updateProviderProperties({ + excluded, + field, + operator, + providerId, + timeline, + value, + }), + }, + }; +}; + +interface UpdateTimelineProviderKqlQueryParams { + id: string; + providerId: string; + kqlQuery: string; + timelineById: TimelineById; +} + +export const updateTimelineProviderKqlQuery = ({ + id, + providerId, + kqlQuery, + timelineById, +}: UpdateTimelineProviderKqlQueryParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: timeline.dataProviders.map(provider => + provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider + ), + }, + }; +}; + +interface UpdateTimelineItemsPerPageParams { + id: string; + itemsPerPage: number; + timelineById: TimelineById; +} + +export const updateTimelineItemsPerPage = ({ + id, + itemsPerPage, + timelineById, +}: UpdateTimelineItemsPerPageParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPage, + }, + }; +}; + +interface UpdateTimelinePageIndexParams { + id: string; + activePage: number; + timelineById: TimelineById; +} + +export const updateTimelinePageIndex = ({ + id, + activePage, + timelineById, +}: UpdateTimelinePageIndexParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + activePage, + }, + }; +}; + +interface UpdateTimelinePerPageOptionsParams { + id: string; + itemsPerPageOptions: number[]; + timelineById: TimelineById; +} + +export const updateTimelinePerPageOptions = ({ + id, + itemsPerPageOptions, + timelineById, +}: UpdateTimelinePerPageOptionsParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPageOptions, + }, + }; +}; + +const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( + p => p.id === andProviderId + ); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + { + ...timeline.dataProviders[providerIndex], + and: [ + ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), + ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), + ], + }, + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +const removeProvider = (providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + ...(timeline.dataProviders[providerIndex].and.length + ? [ + { + ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], + and: [...timeline.dataProviders[providerIndex].and.slice(1)], + }, + ] + : []), + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +interface RemoveTimelineProviderParams { + id: string; + providerId: string; + timelineById: TimelineById; + andProviderId?: string; +} + +export const removeTimelineProvider = ({ + id, + providerId, + timelineById, + andProviderId, +}: RemoveTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? removeAndProvider(andProviderId, providerId, timeline) + : removeProvider(providerId, timeline), + }, + }; +}; + +interface SetDeletedTimelineEventsParams { + id: string; + eventIds: string[]; + isDeleted: boolean; + timelineById: TimelineById; +} + +export const setDeletedTimelineEvents = ({ + id, + eventIds, + isDeleted, + timelineById, +}: SetDeletedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const deletedEventIds = isDeleted + ? union(timeline.deletedEventIds, eventIds) + : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + const selectedEventIds = Object.fromEntries( + Object.entries(timeline.selectedEventIds).filter( + ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) + ) + ); + + const isSelectAllChecked = + Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; + + return { + ...timelineById, + [id]: { + ...timeline, + deletedEventIds, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface SetLoadingTimelineEventsParams { + id: string; + eventIds: string[]; + isLoading: boolean; + timelineById: TimelineById; +} + +export const setLoadingTimelineEvents = ({ + id, + eventIds, + isLoading, + timelineById, +}: SetLoadingTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const loadingEventIds = isLoading + ? union(timeline.loadingEventIds, eventIds) + : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + return { + ...timelineById, + [id]: { + ...timeline, + loadingEventIds, + }, + }; +}; + +interface SetSelectedTimelineEventsParams { + id: string; + eventIds: Record<string, TimelineNonEcsData[]>; + isSelectAllChecked: boolean; + isSelected: boolean; + timelineById: TimelineById; +} + +export const setSelectedTimelineEvents = ({ + id, + eventIds, + isSelectAllChecked = false, + isSelected, + timelineById, +}: SetSelectedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const selectedEventIds = isSelected + ? { ...timeline.selectedEventIds, ...eventIds } + : omit(Object.keys(eventIds), timeline.selectedEventIds); + + return { + ...timelineById, + [id]: { + ...timeline, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface UnPinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const unPinTimelineEvent = ({ + id, + eventId, + timelineById, +}: UnPinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: omit(eventId, timeline.pinnedEventIds), + }, + }; +}; + +interface UpdateHighlightedDropAndProviderIdParams { + id: string; + providerId: string; + timelineById: TimelineById; +} + +export const updateHighlightedDropAndProvider = ({ + id, + providerId, + timelineById, +}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + highlightedDropAndProviderId: providerId, + }, + }; +}; + +interface UpdateSavedQueryParams { + id: string; + savedQueryId: string | null; + timelineById: TimelineById; +} + +export const updateSavedQuery = ({ + id, + savedQueryId, + timelineById, +}: UpdateSavedQueryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + savedQueryId, + }, + }; +}; + +interface UpdateFiltersParams { + id: string; + filters: Filter[]; + timelineById: TimelineById; +} + +export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + filters, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/index.ts b/x-pack/plugins/siem/public/store/timeline/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/index.ts rename to x-pack/plugins/siem/public/store/timeline/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/manage_timeline_id.tsx b/x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/manage_timeline_id.tsx rename to x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts new file mode 100644 index 0000000000000..15bd2980e4aeb --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../components/timeline/data_providers/data_provider'; +import { Sort } from '../../components/timeline/body/sort'; +import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; +import { KueryFilterQuery, SerializedFilterQuery } from '../model'; + +export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages +export type KqlMode = 'filter' | 'search'; +export type EventType = 'all' | 'raw' | 'signal'; + +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; + +/** Uniquely identifies a column */ +export type ColumnId = string; + +/** The specification of a column header */ +export interface ColumnHeaderOptions { + aggregatable?: boolean; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string; + example?: string; + format?: string; + id: ColumnId; + label?: string; + linkField?: string; + placeholder?: string; + type?: string; + width: number; +} + +export interface TimelineModel { + /** The columns displayed in the timeline */ + columns: ColumnHeaderOptions[]; + /** The sources of the event data shown in the timeline */ + dataProviders: DataProvider[]; + /** Events to not be rendered **/ + deletedEventIds: string[]; + /** A summary of the events and notes in this timeline */ + description: string; + /** Typoe of event you want to see in this timeline */ + eventType?: EventType; + /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ + eventIdToNoteIds: Record<string, string[]>; + filters?: Filter[]; + /** The chronological history of actions related to this timeline */ + historyIds: string[]; + /** The chronological history of actions related to this timeline */ + highlightedDropAndProviderId: string; + /** Uniquely identifies the timeline */ + id: string; + /** If selectAll checkbox in header is checked **/ + isSelectAllChecked: boolean; + /** Events to be rendered as loading **/ + loadingEventIds: string[]; + savedObjectId: string | null; + /** When true, this timeline was marked as "favorite" by the user */ + isFavorite: boolean; + /** When true, the timeline will update as new data arrives */ + isLive: boolean; + /** The number of items to show in a single page of results */ + itemsPerPage: number; + /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ + itemsPerPageOptions: number[]; + /** determines the behavior of the KQL bar */ + kqlMode: KqlMode; + /** the KQL query in the KQL bar */ + kqlQuery: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + /** Title */ + title: string; + /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ + noteIds: string[]; + /** Events pinned to this timeline */ + pinnedEventIds: Record<string, boolean>; + pinnedEventsSaveObject: Record<string, PinnedEvent>; + /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ + dateRange: { + start: number; + end: number; + }; + savedQueryId?: string | null; + /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ + selectedEventIds: Record<string, TimelineNonEcsData[]>; + /** When true, show the timeline flyover */ + show: boolean; + /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ + showCheckboxes: boolean; + /** When true, shows additional rowRenderers below the PlainRowRenderer **/ + showRowRenderers: boolean; + /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ + sort: Sort; + /** Persists the UI state (width) of the timeline flyover */ + width: number; + /** timeline is saving */ + isSaving: boolean; + isLoading: boolean; + version: string | null; +} + +export type SubsetTimelineModel = Readonly< + Pick< + TimelineModel, + | 'columns' + | 'dataProviders' + | 'deletedEventIds' + | 'description' + | 'eventType' + | 'eventIdToNoteIds' + | 'highlightedDropAndProviderId' + | 'historyIds' + | 'isFavorite' + | 'isLive' + | 'isSelectAllChecked' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'kqlMode' + | 'kqlQuery' + | 'title' + | 'loadingEventIds' + | 'noteIds' + | 'pinnedEventIds' + | 'pinnedEventsSaveObject' + | 'dateRange' + | 'selectedEventIds' + | 'show' + | 'showCheckboxes' + | 'showRowRenderers' + | 'sort' + | 'width' + | 'isSaving' + | 'isLoading' + | 'savedObjectId' + | 'version' + > +>; + +export interface TimelineUrl { + id: string; + isOpen: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/my_epic_timeline_id.ts b/x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/my_epic_timeline_id.ts rename to x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/reducer.test.ts rename to x-pack/plugins/siem/public/store/timeline/reducer.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts b/x-pack/plugins/siem/public/store/timeline/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts rename to x-pack/plugins/siem/public/store/timeline/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/refetch_queries.ts b/x-pack/plugins/siem/public/store/timeline/refetch_queries.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/refetch_queries.ts rename to x-pack/plugins/siem/public/store/timeline/refetch_queries.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts b/x-pack/plugins/siem/public/store/timeline/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts rename to x-pack/plugins/siem/public/store/timeline/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts b/x-pack/plugins/siem/public/store/timeline/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/types.ts rename to x-pack/plugins/siem/public/store/timeline/types.ts diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts new file mode 100644 index 0000000000000..2c679ba41116e --- /dev/null +++ b/x-pack/plugins/siem/public/store/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/plugins/siem/public/utils/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/api/index.ts rename to x-pack/plugins/siem/public/utils/api/index.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/apollo_context.ts b/x-pack/plugins/siem/public/utils/apollo_context.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/apollo_context.ts rename to x-pack/plugins/siem/public/utils/apollo_context.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/plugins/siem/public/utils/default_date_settings.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts rename to x-pack/plugins/siem/public/utils/default_date_settings.test.ts index bb66067512d1f..9dc179ba7a6e2 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts +++ b/x-pack/plugins/siem/public/utils/default_date_settings.test.ts @@ -21,7 +21,7 @@ import { DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, DEFAULT_INTERVAL_TYPE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; @@ -30,7 +30,7 @@ import { Policy } from '../store/inputs/model'; // we have to repeat ourselves once const DEFAULT_FROM_DATE = '1983-05-31T13:03:54.234Z'; const DEFAULT_TO_DATE = '1990-05-31T13:03:54.234Z'; -jest.mock('../../../../../plugins/siem/common/constants', () => ({ +jest.mock('../../common/constants', () => ({ DEFAULT_FROM: '1983-05-31T13:03:54.234Z', DEFAULT_TO: '1990-05-31T13:03:54.234Z', DEFAULT_INTERVAL_PAUSE: true, diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts b/x-pack/plugins/siem/public/utils/default_date_settings.ts similarity index 98% rename from x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts rename to x-pack/plugins/siem/public/utils/default_date_settings.ts index 89f7d34d85131..c4869a4851ae5 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts +++ b/x-pack/plugins/siem/public/utils/default_date_settings.ts @@ -15,7 +15,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx rename to x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx rename to x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/logo_endpoint/64_color.svg b/x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/logo_endpoint/64_color.svg rename to x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/plugins/siem/public/utils/route/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/helpers.ts rename to x-pack/plugins/siem/public/utils/route/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx b/x-pack/plugins/siem/public/utils/route/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx rename to x-pack/plugins/siem/public/utils/route/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx rename to x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/plugins/siem/public/utils/route/spy_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx rename to x-pack/plugins/siem/public/utils/route/spy_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/plugins/siem/public/utils/route/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/types.ts rename to x-pack/plugins/siem/public/utils/route/types.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx b/x-pack/plugins/siem/public/utils/route/use_route_spy.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx rename to x-pack/plugins/siem/public/utils/route/use_route_spy.tsx diff --git a/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx new file mode 100644 index 0000000000000..335398177f0f4 --- /dev/null +++ b/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; +import { + SavedQueryService, + createSavedQueryService, +} from '../../../../../../src/plugins/data/public'; + +import { useKibana } from '../../lib/kibana'; + +export const useSavedQueryServices = () => { + const kibana = useKibana(); + const client = kibana.services.savedObjects.client; + + const [savedQueryService, setSavedQueryService] = useState<SavedQueryService>( + createSavedQueryService(client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(client)); + }, [client]); + return savedQueryService; +}; diff --git a/x-pack/legacy/plugins/siem/public/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/timeline/use_show_timeline.tsx rename to x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts b/x-pack/plugins/siem/public/utils/use_mount_appended.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts rename to x-pack/plugins/siem/public/utils/use_mount_appended.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/validators/index.ts b/x-pack/plugins/siem/public/utils/validators/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/validators/index.ts rename to x-pack/plugins/siem/public/utils/validators/index.ts diff --git a/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js index 0b5e5d6cf13b5..9b4a57f09066d 100644 --- a/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js @@ -11,24 +11,23 @@ import madge from 'madge'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import { run, createFailError } from '@kbn/dev-utils'; -const legacyPluginPath = '../../../../legacy/plugins/siem'; -const pluginPath = '../..'; - run( async ({ log }) => { const result = await madge( - [resolve(__dirname, legacyPluginPath, 'public'), resolve(__dirname, pluginPath, 'common')], + [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], excludeRegExp: [ 'test.ts$', 'test.tsx$', 'containers/detection_engine/rules/types.ts$', - 'core/public/chrome/chrome_service.tsx$', 'src/core/server/types.ts$', 'src/core/server/saved_objects/types.ts$', + 'src/core/public/chrome/chrome_service.tsx$', 'src/core/public/overlays/banners/banners_service.tsx$', 'src/core/public/saved_objects/saved_objects_client.ts$', + 'src/plugins/data/public', + 'src/plugins/ui_actions/public', ], } ); diff --git a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js index 145d9715970c8..c8642e8b42c8f 100644 --- a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -12,13 +12,7 @@ const fetch = require('node-fetch'); const { camelCase } = require('lodash'); const { resolve } = require('path'); -const OUTPUT_DIRECTORY = resolve( - '../../legacy/plugins/siem', - 'public', - 'pages', - 'detection_engine', - 'mitre' -); +const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); const MITRE_ENTREPRISE_ATTACK_URL = 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; diff --git a/x-pack/plugins/siem/scripts/generate_types_from_graphql.js b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js index bded8832aba5a..e6b063dfd2c07 100644 --- a/x-pack/plugins/siem/scripts/generate_types_from_graphql.js +++ b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js @@ -10,19 +10,12 @@ const { join, resolve } = require('path'); // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved const { generate } = require('graphql-code-generator'); -const legacyPluginPath = '../../legacy/plugins/siem'; - const GRAPHQL_GLOBS = [ - join(legacyPluginPath, 'public', 'containers', '**', '*.gql_query.ts{,x}'), + join('public', 'containers', '**', '*.gql_query.ts{,x}'), join('common', 'graphql', '**', '*.gql_query.ts{,x}'), ]; -const OUTPUT_INTROSPECTION_PATH = resolve( - legacyPluginPath, - 'public', - 'graphql', - 'introspection.json' -); -const OUTPUT_CLIENT_TYPES_PATH = resolve(legacyPluginPath, 'public', 'graphql', 'types.ts'); +const OUTPUT_INTROSPECTION_PATH = resolve('public', 'graphql', 'introspection.json'); +const OUTPUT_CLIENT_TYPES_PATH = resolve('public', 'graphql', 'types.ts'); const OUTPUT_SERVER_TYPES_PATH = resolve('server', 'graphql', 'types.ts'); const SCHEMA_PATH = resolve(__dirname, 'combined_schema.ts'); diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json index 42d26c4c27ed6..fb89c0e0fc3e2 100644 --- a/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json @@ -2,7 +2,6 @@ "include": [ "typings/**/*", "plugins/siem/**/*", - "legacy/plugins/siem/**/*", "plugins/apm/typings/numeral.d.ts", "legacy/plugins/canvas/types/webpack.d.ts", "plugins/triggers_actions_ui/**/*" diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 9dd04247b7f47..bc2b3a53d85f3 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -125,6 +125,11 @@ export const timelineSchema = gql` script: String } + enum TimelineType { + default + template + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] @@ -134,6 +139,9 @@ export const timelineSchema = gql` kqlMode: String kqlQuery: SerializedFilterQueryInput title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String sort: SortTimelineInput @@ -237,6 +245,9 @@ export const timelineSchema = gql` savedObjectId: String! sort: SortTimelineResult title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType updated: Float updatedBy: String version: String! diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index d272b7ff59b79..6a35ba08f8e43 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -134,6 +134,12 @@ export interface TimelineInput { title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + dateRange?: Maybe<DateRangePickerInput>; savedQueryId?: Maybe<string>; @@ -336,6 +342,11 @@ export enum TlsFields { _id = '_id', } +export enum TimelineType { + default = 'default', + template = 'template', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -1946,6 +1957,12 @@ export interface TimelineResult { title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + updated?: Maybe<number>; updatedBy?: Maybe<string>; @@ -8023,6 +8040,12 @@ export namespace TimelineResultResolvers { title?: TitleResolver<Maybe<string>, TypeParent, TContext>; + templateTimelineId?: TemplateTimelineIdResolver<Maybe<string>, TypeParent, TContext>; + + templateTimelineVersion?: TemplateTimelineVersionResolver<Maybe<number>, TypeParent, TContext>; + + timelineType?: TimelineTypeResolver<Maybe<TimelineType>, TypeParent, TContext>; + updated?: UpdatedResolver<Maybe<number>, TypeParent, TContext>; updatedBy?: UpdatedByResolver<Maybe<string>, TypeParent, TContext>; @@ -8130,6 +8153,21 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver<R, Parent, TContext>; + export type TemplateTimelineIdResolver< + R = Maybe<string>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TemplateTimelineVersionResolver< + R = Maybe<number>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TimelineTypeResolver< + R = Maybe<TimelineType>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; export type UpdatedResolver< R = Maybe<number>, Parent = TimelineResult, diff --git a/x-pack/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index 9c46f3320e37e..4a595032e43eb 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 d50c339c95266..e50f82bb482a7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -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 750c5574f4a72..2028216e6770f 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_instances.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh index 8d8cbdd70a803..c587e9a204182 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_types.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/types \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index a56d788d69c16..22b602f935187 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -9,9 +9,15 @@ set -e ./check_env_variables.sh +# Clean up and remove all actions and alerts from SIEM +# within saved objects ./delete_all_actions.sh ./delete_all_alerts.sh ./delete_all_alert_tasks.sh + +# delete all the statuses from the signal index ./delete_all_statuses.sh + +# re-create the signal index ./delete_signal_index.sh ./post_signal_index.sh diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d298f1cc7cbc6..a8cc6dc680410 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow, set, omit } from 'lodash/fp'; +import { flow, omit } from 'lodash/fp'; +import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; @@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { } const omitDottedFields = omit(errantFields.map(field => field.name)); - const setNestedFields = errantFields.map(field => set(field.name, field.value)); - const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) => + set(_anomaly, field.name, field.value) + ); + const setTimestamp = (_anomaly: Anomaly) => + set(_anomaly, '@timestamp', new Date(timestamp).toISOString()); return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 3eae30625e422..219465f551457 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -15,6 +15,11 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { + SavedNote, + NoteSavedObjectRuntimeType, + NoteSavedObject, +} from '../../../common/types/timeline/note'; import { PageInfoNote, ResponseNote, @@ -23,178 +28,198 @@ import { NoteResult, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; import { 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 f7a10317bd84d..0000000000000 --- a/x-pack/plugins/siem/server/lib/note/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; - -/* - * Note Types - */ -export const SavedNoteRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - timelineId: unionWithNullType(runtimeTypes.string), - }), - runtimeTypes.partial({ - eventId: unionWithNullType(runtimeTypes.string), - note: unionWithNullType(runtimeTypes.string), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), - }), -]); - -export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {} - -/** - * Note Saved object type with metadata - */ - -export const NoteSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedNoteRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - noteId: runtimeTypes.string, - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ - SavedNoteRuntimeType, - runtimeTypes.type({ - noteId: runtimeTypes.string, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export interface NoteSavedObject - extends runtimeTypes.TypeOf<typeof NoteSavedObjectToReturnRuntimeType> {} diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts index 1e3a481e17106..c653f23d5c149 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -13,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 e598f03935047..0000000000000 --- a/x-pack/plugins/siem/server/lib/pinned_event/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; - -/* - * Note Types - */ -export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - timelineId: runtimeTypes.string, - eventId: runtimeTypes.string, - }), - runtimeTypes.partial({ - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), - }), -]); - -export interface SavedPinnedEvent extends runtimeTypes.TypeOf<typeof SavedPinnedEventRuntimeType> {} - -/** - * Note Saved object type with metadata - */ - -export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedPinnedEventRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - pinnedEventId: unionWithNullType(runtimeTypes.string), - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - pinnedEventId: runtimeTypes.string, - version: runtimeTypes.string, - }), - SavedPinnedEventRuntimeType, - runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export interface PinnedEventSavedObject - extends runtimeTypes.TypeOf<typeof PinnedEventToReturnSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index ea5db565483c8..bde24a338ec84 100644 --- a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -8,16 +8,21 @@ import { failure } from 'io-ts/lib/PathReporter'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { TimelineSavedObjectRuntimeType, TimelineSavedObject } from './types'; +import { + TimelineSavedObjectRuntimeType, + TimelineSavedObject, +} from '../../../common/types/timeline'; export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => { const timeline = pipe( TimelineSavedObjectRuntimeType.decode(savedObject), - map(savedTimeline => ({ - savedObjectId: savedTimeline.id, - version: savedTimeline.version, - ...savedTimeline.attributes, - })), + map(savedTimeline => { + return { + savedObjectId: savedTimeline.id, + version: savedTimeline.version, + ...savedTimeline.attributes, + }; + }), fold(errors => { throw new Error(failure(errors).join('\n')); }, identity) diff --git a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts index abe8de9bf5b94..6b4017b5e4d5c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts @@ -22,6 +22,7 @@ import { import { ImportTimelineResponse } from './routes/utils/import_timelines'; import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; +import { BadRequestError } from '../detection_engine/errors/bad_request_error'; type ErrorFactory = (message: string) => Error; @@ -38,8 +39,11 @@ export const decodeOrThrow = <A, O, I>( pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); export const validateTimelines = (): Transform => - createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj)); - + createMapStream((obj: ImportTimelineResponse) => + obj instanceof Error + ? new BadRequestError(obj.message) + : decodeOrThrow(ImportTimelinesSchemaRt)(obj) + ); export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 19adb7ac1045a..eeded1cc2532d 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { SavedTimeline } from './types'; +import { SavedTimeline, TimelineType } from '../../../common/types/timeline'; export const pickSavedTimeline = ( timelineId: string | null, @@ -24,5 +25,21 @@ export const pickSavedTimeline = ( savedTimeline.updated = dateNow; savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } + + if (savedTimeline.timelineType === TimelineType.template) { + savedTimeline.timelineType = TimelineType.template; + if (savedTimeline.templateTimelineId == null) { + savedTimeline.templateTimelineId = uuid.v4(); + } + + if (savedTimeline.templateTimelineVersion == null) { + savedTimeline.templateTimelineVersion = 1; + } + } else { + savedTimeline.timelineType = TimelineType.default; + savedTimeline.templateTimelineId = null; + savedTimeline.templateTimelineVersion = null; + } + return savedTimeline; }; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/README.md b/x-pack/plugins/siem/server/lib/timeline/routes/README.md new file mode 100644 index 0000000000000..2c5547e39fc4e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/README.md @@ -0,0 +1,326 @@ +**Timeline apis** + + 1. Create timeline api + 2. Update timeline api + 3. Create template timeline api + 4. Update template timeline api + + +## Create timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + } + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "created": 1587468588922, + "createdBy": "casetester", + "updated": 1587468588922, + "updatedBy": "casetester", + "timelineType": "default" + }, + "timelineId":"68ea5330-83c3-11ea-bff9-ab01dd7cb6cc", // Have to match the existing timeline savedObject id + "version":"WzYwLDFd" // Have to match the existing timeline version +} +``` + +## Create template timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [ + + ], + "description": "", + "eventType": "all", + "filters": [ + + ], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template" // This is the difference between create timeline + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update template timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template", + "created": 1587473119992, + "createdBy": "casetester", + "updated": 1587473119992, + "updatedBy": "casetester", + "templateTimelineId": "745d0316-6af7-43bf-afd6-9747119754fb", // Please provide the existing template timeline version + "templateTimelineVersion": 2 // Please provide a template timeline version grater than existing one + }, + "timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well + "version":"Wzg2LDFd" // Please provide the existing timeline version +} +``` \ No newline at end of file diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index 686f2b491cf88..a832c818d48b0 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,6 +5,7 @@ */ import { omit } from 'lodash/fp'; +import { TimelineType } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -148,6 +149,13 @@ export const mockGetTimelineValue = { pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'], }; +export const mockGetTemplateTimelineValue = { + ...mockGetTimelineValue, + timelineType: TimelineType.template, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 1, +}; + export const mockParsedTimelineObject = omit( [ 'globalNotes', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index a83c443773302..304ca309775ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -3,10 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; -import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import * as rt from 'io-ts'; +import { + TIMELINE_EXPORT_URL, + TIMELINE_IMPORT_URL, + TIMELINE_URL, +} from '../../../../../common/constants'; import stream from 'stream'; +import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import { SavedTimeline, TimelineType } from '../../../../../common/types/timeline'; +import { updateTimelineSchema } from '../schemas/update_timelines_schema'; +import { createTimelineSchema } from '../schemas/create_timelines_schema'; + const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ @@ -31,6 +39,96 @@ export const getImportTimelinesRequest = (filename?: string) => }, }); +export const inputTimeline: SavedTimeline = { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp' }, + { columnHeaderType: 'not-filtered', id: 'message' }, + { columnHeaderType: 'not-filtered', id: 'event.category' }, + { columnHeaderType: 'not-filtered', id: 'event.action' }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: null }, + title: 't', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { start: 1585227005527, end: 1585313405527 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, +}; + +export const inputTemplateTimeline = { + ...inputTimeline, + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, +}; + +export const createTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.default, +}; + +export const createTemplateTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTemplateTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.template, +}; + +export const createTimelineWithTimelineId = { + ...createTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', +}; + +export const createTemplateTimelineWithTimelineId = { + ...createTemplateTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineId: 'existing template timeline id', +}; + +export const updateTimelineWithTimelineId = { + timeline: inputTimeline, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const updateTemplateTimelineWithTimelineId = { + timeline: { + ...inputTemplateTimeline, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 2, + }, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const getCreateTimelinesRequest = (mockBody: rt.TypeOf<typeof createTimelineSchema>) => + requestMock.create({ + method: 'post', + path: TIMELINE_URL, + body: mockBody, + }); + +export const getUpdateTimelinesRequest = (mockBody: rt.TypeOf<typeof updateTimelineSchema>) => + requestMock.create({ + method: 'patch', + path: TIMELINE_URL, + body: mockBody, + }); + export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => requestMock.create({ method: 'post', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts new file mode 100644 index 0000000000000..70ee1532395a5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + getCreateTimelinesRequest, + inputTimeline, + createTimelineWithoutTimelineId, + createTimelineWithTimelineId, + createTemplateTimelineWithoutTimelineId, + createTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +describe('create timelines', () => { + let server: ReturnType<typeof serverMock.create>; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Create a new timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new timeline savedObject without timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Create a new template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTemplateTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTemplateTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new template timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new template timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new template timeline savedObject without template timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new template timeline savedObject witn given template timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + createTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a template timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts new file mode 100644 index 0000000000000..c456ae31fb7da --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { ConfigType } from '../../..'; +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; + +import { createTimelineSchema } from './schemas/create_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { + createTimelines, + getTimeline, + getTemplateTimeline, + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +export const createTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.post( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(createTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + + if ( + (!isHandlingTemplateTimeline && existTimeline != null) || + (isHandlingTemplateTimeline && (existTemplateTimeline != null || existTimeline != null)) + ) { + return siemResponse.error({ + body: isHandlingTemplateTimeline + ? CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE + : CREATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + } + + // Create timeline + const newTimeline = await createTimelines(frameworkRequest, timeline, null, version); + return response.ok({ + body: { + data: { + persistTimeline: newTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 9f41943cfa27f..56c152d02ae98 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -6,13 +6,13 @@ import { getImportTimelinesRequest } from './__mocks__/request_responses'; import { - createMockConfig, serverMock, requestContextMock, requestMock, + createMockConfig, } from '../../detection_engine/routes/__mocks__'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { SecurityPluginSetup } from '../../../../../security/server'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; import { mockUniqueParsedObjects, @@ -24,7 +24,6 @@ import { } from './__mocks__/import_timelines'; describe('import timelines', () => { - let config: ReturnType<typeof createMockConfig>; let server: ReturnType<typeof serverMock.create>; let request: ReturnType<typeof requestMock.create>; let securitySetup: SecurityPluginSetup; @@ -43,7 +42,6 @@ describe('import timelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - config = createMockConfig(); securitySetup = ({ authc: { @@ -84,40 +82,28 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { @@ -230,38 +216,26 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), - persistTimeline: mockPersistTimeline, - }; - }), + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('returns error message', async () => { @@ -286,36 +260,24 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( - new Error('Test error') - ), - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( + new Error('Test error') + ), }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); }); @@ -328,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 9d148abf82cdd..bff89bdf9b5b2 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -5,9 +5,19 @@ */ import { extname } from 'path'; -import { chunk, omit, set } from 'lodash/fp'; +import { chunk, omit } from 'lodash/fp'; + +import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; + +import { SetupPlugins } from '../../../plugin'; +import { ConfigType } from '../../../config'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; +import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, createBulkErrorObject, @@ -16,32 +26,22 @@ import { } from '../../detection_engine/routes/utils'; import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; import { - createTimelines, getTupleDuplicateErrorsAndUniqueTimeline, isBulkError, isImportRegular, ImportTimelineResponse, ImportTimelinesSchema, PromiseFromStreams, + timelineSavedObjectOmittedFields, } from './utils/import_timelines'; +import { createTimelines, getTimeline } from './utils/create_timelines'; -import { IRouter } from '../../../../../../../src/core/server'; -import { SetupPlugins } from '../../../plugin'; -import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { ConfigType } from '../../../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 0000000000000..241d266a14c78 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const createTimelineSchema = rt.intersection([ + rt.type({ + timeline: SavedTimelineRuntimeType, + }), + rt.partial({ + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), + }), +]); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts index 056fdaf0d2515..3b340b1c15359 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -7,14 +7,17 @@ import * as rt from 'io-ts'; import { Readable } from 'stream'; import { either } from 'fp-ts/lib/Either'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; + import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; -import { SavedTimelineRuntimeType } from '../../types'; +import { unionWithNullType } from '../../../../../common/utility_types'; export const ImportTimelinesSchemaRt = rt.intersection([ SavedTimelineRuntimeType, rt.type({ - savedObjectId: rt.string, - version: rt.string, + savedObjectId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), }), rt.type({ globalNotes, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 71627363ef0f8..1fd3a3554dc15 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../framework'; -import { SavedNoteRuntimeType } from '../../../note/types'; +import { unionWithNullType } from '../../../../../common/utility_types'; +import { SavedNoteRuntimeType } from '../../../../../common/types/timeline/note'; -export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string)); +export const eventNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const globalNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts new file mode 100644 index 0000000000000..43f4208947aa5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const updateTimelineSchema = rt.type({ + timeline: SavedTimelineRuntimeType, + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts new file mode 100644 index 0000000000000..9c47488d47159 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + getUpdateTimelinesRequest, + inputTimeline, + updateTimelineWithTimelineId, + updateTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + UPDATE_TIMELINE_ERROR_MESSAGE, + UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, +} from './utils/update_timelines'; + +describe('update timelines', () => { + let server: ReturnType<typeof serverMock.create>; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockGetTemplateTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockGetTemplateTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Update an existing timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check a if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual(updateTimelineWithTimelineId.timelineId); + }); + + test('should Update existing timeline savedObject with timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toEqual( + updateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing timeline savedObject with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual(updateTimelineWithTimelineId.version); + }); + + test('should Update existing timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Update an existing template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [mockGetTemplateTimelineValue], + }), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with template timelineId', async () => { + expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual( + updateTemplateTimelineWithTimelineId.version + ); + }); + + test('should Update existing template timeline witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + updateTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create template timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a template timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [], + }), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts new file mode 100644 index 0000000000000..a0f3d11a1533d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../..'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { FrameworkRequest } from '../../framework'; + +import { updateTimelineSchema } from './schemas/update_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { checkIsFailureCases } from './utils/update_timelines'; + +export const updateTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.patch( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(updateTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + // eslint-disable-next-line complexity + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, templateTimelineVersion, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + const errorObj = checkIsFailureCases( + isHandlingTemplateTimeline, + version, + templateTimelineVersion ?? null, + existTimeline, + existTemplateTimeline + ); + if (errorObj != null) { + return siemResponse.error(errorObj); + } + const updatedTimeline = await createTimelines( + (frameworkRequest as unknown) as FrameworkRequest, + timeline, + timelineId, + version + ); + return response.ok({ + body: { + data: { + persistTimeline: updatedTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts new file mode 100644 index 0000000000000..1036a74b74a03 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { set } from 'lodash/fp'; + +import { SetupPlugins } from '../../../../plugin'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { RequestHandlerContext } from '../../../../../../../../target/types/core/server'; +import { FrameworkRequest } from '../../../framework'; + +export const buildFrameworkRequest = async ( + context: RequestHandlerContext, + security: SetupPlugins['security'], + request: KibanaRequest +): Promise<FrameworkRequest> => { + const savedObjectsClient = context.core.savedObjects.client; + const user = await security?.authc.getCurrentUser(request); + + return set<FrameworkRequest>( + 'user', + user, + set<KibanaRequest & { context: RequestHandlerContext }>( + 'context.core.savedObjects.client', + savedObjectsClient, + request + ) + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts new file mode 100644 index 0000000000000..2c67a514cdf97 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isEmpty } from 'lodash/fp'; + +import * as timelineLib from '../../saved_object'; +import * as pinnedEventLib from '../../../pinned_event/saved_object'; +import * as noteLib from '../../../note/saved_object'; +import { FrameworkRequest } from '../../../framework'; +import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline'; +import { SavedNote } from '../../../../../common/types/timeline/note'; +import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; +export const CREATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE timeline with POST is not allowed, please use PATCH instead'; +export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; + +export const saveTimelines = ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null +): Promise<ResponseTimeline> => { + return timelineLib.persistTimeline( + frameworkRequest, + timelineSavedObjectId ?? null, + timelineVersion ?? null, + timeline + ); +}; + +export const savePinnedEvents = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + pinnedEventIds: string[] +) => + Promise.all( + pinnedEventIds.map(eventId => + pinnedEventLib.persistPinnedEventOnTimeline( + frameworkRequest, + null, // pinnedEventSavedObjectId + eventId, + timelineSavedObjectId + ) + ) + ); + +export const saveNotes = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[] +) => { + return Promise.all( + newNotes?.map(note => { + const newNote: SavedNote = { + eventId: note.eventId, + note: note.note, + timelineId: timelineSavedObjectId, + }; + + return noteLib.persistNote( + frameworkRequest, + existingNoteIds?.find(nId => nId === note.noteId) ?? null, + timelineVersion ?? null, + newNote + ); + }) ?? [] + ); +}; + +export const createTimelines = async ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null, + pinnedEventIds?: string[] | null, + notes?: NoteResult[], + existingNoteIds?: string[] +): Promise<ResponseTimeline> => { + const responseTimeline = await saveTimelines( + frameworkRequest, + timeline, + timelineSavedObjectId, + timelineVersion + ); + const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; + const newTimelineVersion = responseTimeline.timeline.version; + + let myPromises: unknown[] = []; + if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { + myPromises = [ + ...myPromises, + savePinnedEvents( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + pinnedEventIds + ), + ]; + } + if (!isEmpty(notes)) { + myPromises = [ + ...myPromises, + saveNotes( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + newTimelineVersion, + existingNoteIds, + notes + ), + ]; + } + + if (myPromises.length > 0) { + await Promise.all(myPromises); + } + + return responseTimeline; +}; + +export const getTimeline = async ( + frameworkRequest: FrameworkRequest, + savedObjectId: string +): Promise<TimelineSavedObject | null> => { + let timeline = null; + try { + timeline = await timelineLib.getTimeline(frameworkRequest, savedObjectId); + // eslint-disable-next-line no-empty + } catch (e) {} + return timeline; +}; + +export const getTemplateTimeline = async ( + frameworkRequest: FrameworkRequest, + templateTimelineId: string +): Promise<TimelineSavedObject | null> => { + let templateTimeline = null; + try { + templateTimeline = await timelineLib.getTimelineByTemplateTimelineId( + frameworkRequest, + templateTimelineId + ); + } catch (e) { + return null; + } + return templateTimeline.timeline[0]; +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index 677891fa16c02..ea9a5fab66805 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -4,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 f69a715f9b2c9..9e120cdc023dc 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -7,20 +7,10 @@ import uuid from 'uuid'; import { has } from 'lodash/fp'; import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { PinnedEvent } from '../../../pinned_event/saved_object'; -import { Note } from '../../../note/saved_object'; - -import { Timeline } from '../../saved_object'; -import { SavedTimeline } from '../../types'; -import { FrameworkRequest } from '../../../framework'; -import { SavedNote } from '../../../note/types'; +import { SavedTimeline } from '../../../../../common/types/timeline'; import { NoteResult } from '../../../../graphql/types'; import { HapiReadableStream } from '../../../detection_engine/rules/types'; -const pinnedEventLib = new PinnedEvent(); -const timelineLib = new Timeline(); -const noteLib = new Note(); - export interface ImportTimelinesSchema { success: boolean; success_count: number; @@ -84,100 +74,6 @@ export const getTupleDuplicateErrorsAndUniqueTimeline = ( return [Array.from(errors.values()), Array.from(timelinesAcc.values())]; }; -export const saveTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null -) => { - const newTimelineRes = await timelineLib.persistTimeline( - frameworkRequest, - timelineSavedObjectId ?? null, - timelineVersion ?? null, - timeline - ); - - return { - newTimelineSavedObjectId: newTimelineRes?.timeline?.savedObjectId ?? null, - newTimelineVersion: newTimelineRes?.timeline?.version ?? null, - }; -}; - -export const savePinnedEvents = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - pinnedEventIds?: string[] | null -) => { - return ( - pinnedEventIds?.map(eventId => { - return pinnedEventLib.persistPinnedEventOnTimeline( - frameworkRequest, - null, // pinnedEventSavedObjectId - eventId, - timelineSavedObjectId - ); - }) ?? [] - ); -}; - -export const saveNotes = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map(note => { - const newNote: SavedNote = { - eventId: note.eventId, - note: note.note, - timelineId: timelineSavedObjectId, - }; - - return noteLib.persistNote( - frameworkRequest, - existingNoteIds?.find(nId => nId === note.noteId) ?? null, - timelineVersion ?? null, - newNote - ); - }) ?? [] - ); -}; - -export const createTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null, - pinnedEventIds?: string[] | null, - notes?: NoteResult[], - existingNoteIds?: string[] -) => { - const { newTimelineSavedObjectId, newTimelineVersion } = await saveTimelines( - frameworkRequest, - timeline, - timelineSavedObjectId, - timelineVersion - ); - await Promise.all([ - savePinnedEvents( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - pinnedEventIds - ), - saveNotes( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - newTimelineVersion, - existingNoteIds, - notes - ), - ]); - - return newTimelineSavedObjectId; -}; - export const isImportRegular = ( importTimelineResponse: ImportTimelineResponse ): importTimelineResponse is ImportRegular => { @@ -189,3 +85,15 @@ export const isBulkError = ( ): importRuleResponse is BulkError => { return has('error', importRuleResponse); }; + +export const timelineSavedObjectOmittedFields = [ + 'globalNotes', + 'eventNotes', + 'pinnedEventIds', + 'version', + 'savedObjectId', + 'created', + 'createdBy', + 'updated', + 'updatedBy', +]; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts new file mode 100644 index 0000000000000..6a25d8def9116 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const UPDATE_TIMELINE_ERROR_MESSAGE = + 'CREATE timeline with PATCH is not allowed, please use POST instead'; +export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'CREATE template timeline with PATCH is not allowed, please use POST instead'; +export const NO_MATCH_VERSION_ERROR_MESSAGE = + 'TimelineVersion conflict: The given version doesn not match with existing timeline'; +export const NO_MATCH_ID_ERROR_MESSAGE = + "Timeline id doesn't match with existing template timeline"; +export const OLDER_VERSION_ERROR_MESSAGE = + 'Template timelineVersion conflict: The given version is older then existing version'; + +export const checkIsFailureCases = ( + isHandlingTemplateTimeline: boolean, + version: string | null, + templateTimelineVersion: number | null, + existTimeline: TimelineSavedObject | null, + existTemplateTimeline: TimelineSavedObject | null +) => { + if (!isHandlingTemplateTimeline && existTimeline == null) { + return { + body: UPDATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if (isHandlingTemplateTimeline && existTemplateTimeline == null) { + // Throw error to create template timeline in patch + return { + body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if ( + isHandlingTemplateTimeline && + existTimeline != null && + existTemplateTimeline != null && + existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId + ) { + // Throw error you can not have a no matching between your timeline and your template timeline during an update + return { + body: NO_MATCH_ID_ERROR_MESSAGE, + statusCode: 409, + }; + } else if (!isHandlingTemplateTimeline && existTimeline?.version !== version) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion == null && + existTemplateTimeline.version !== version + ) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + templateTimelineVersion != null && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion != null && + existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion + ) { + // Throw error you can not update a template timeline version with an old version + return { + body: OLDER_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else { + return null; + } +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index e8cd27947589f..d2df7589f3c4a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -8,260 +8,308 @@ import { getOr } from 'lodash/fp'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { NoteSavedObject } from '../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; +import { SavedTimeline, TimelineSavedObject, TimelineType } from '../../../common/types/timeline'; import { ResponseTimeline, PageInfoTimeline, SortTimeline, ResponseFavoriteTimeline, TimelineResult, + Maybe, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { Note } from '../note/saved_object'; -import { NoteSavedObject } from '../note/types'; -import { PinnedEventSavedObject } from '../pinned_event/types'; -import { PinnedEvent } from '../pinned_event/saved_object'; +import * as note from '../note/saved_object'; +import * as pinnedEvent from '../pinned_event/saved_object'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { pickSavedTimeline } from './pick_saved_timeline'; import { timelineSavedObjectType } from './saved_object_mappings'; -import { SavedTimeline, TimelineSavedObject } from './types'; interface ResponseTimelines { timeline: TimelineSavedObject[]; totalCount: number; } -export class Timeline { - private readonly note = new Note(); - private readonly pinnedEvent = new PinnedEvent(); +export interface ResponseTemplateTimeline { + code?: Maybe<number>; - public async getTimeline( - request: FrameworkRequest, - timelineId: string - ): Promise<TimelineSavedObject> { - return this.getSavedTimeline(request, timelineId); - } + message?: Maybe<string>; + + templateTimeline: TimelineResult; +} - public async getAllTimeline( +export interface Timeline { + getTimeline: (request: FrameworkRequest, timelineId: string) => Promise<TimelineSavedObject>; + + getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null - ): Promise<ResponseTimelines> { - const options: SavedObjectsFindOptions = { - type: timelineSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: onlyUserFavorite - ? ['title', 'description', 'favorite.keySearch'] - : ['title', 'description'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - - return this.getAllSavedTimeline(request, options); - } + ) => Promise<ResponseTimelines>; - public async persistFavorite( + persistFavorite: ( request: FrameworkRequest, timelineId: string | null - ): Promise<ResponseFavoriteTimeline> { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const fullName = request.user?.full_name ?? ''; - try { - let timeline: SavedTimeline = {}; - if (timelineId != null) { - const { - eventIdToNoteIds, - notes, - noteIds, - pinnedEventIds, - pinnedEventsSaveObject, - savedObjectId, - version, - ...savedTimeline - } = await this.getBasicSavedTimeline(request, timelineId); - timelineId = savedObjectId; // eslint-disable-line no-param-reassign - timeline = savedTimeline; - } + ) => Promise<ResponseFavoriteTimeline>; - const userFavoriteTimeline = { - keySearch: userName != null ? convertStringToBase64(userName) : null, - favoriteDate: new Date().valueOf(), - fullName, - userName, - }; - if (timeline.favorite != null) { - const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( - user => user.userName === userName - ); - - timeline.favorite = - alreadyExistsTimelineFavoriteByUser > -1 - ? [ - ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), - ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), - ] - : [...timeline.favorite, userFavoriteTimeline]; - } else if (timeline.favorite == null) { - timeline.favorite = [userFavoriteTimeline]; - } + persistTimeline: ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline, + timelineType?: TimelineType | null + ) => Promise<ResponseTimeline>; - const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); + deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise<void>; + convertStringToBase64: (text: string) => string; + timelineWithReduxProperties: ( + notes: NoteSavedObject[], + pinnedEvents: PinnedEventSavedObject[], + timeline: TimelineSavedObject, + userName: string + ) => TimelineSavedObject; +} + +export const getTimeline = async ( + request: FrameworkRequest, + timelineId: string +): Promise<TimelineSavedObject> => { + return getSavedTimeline(request, timelineId); +}; + +export const getTimelineByTemplateTimelineId = async ( + request: FrameworkRequest, + templateTimelineId: string +): Promise<{ + totalCount: number; + timeline: TimelineSavedObject[]; +}> => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + filter: `siem-ui-timeline.attributes.templateTimelineId: ${templateTimelineId}`, + }; + return getAllSavedTimeline(request, options); +}; + +export const getAllTimeline = async ( + request: FrameworkRequest, + onlyUserFavorite: boolean | null, + pageInfo: PageInfoTimeline | null, + search: string | null, + sort: SortTimeline | null +): Promise<ResponseTimelines> => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: onlyUserFavorite + ? ['title', 'description', 'favorite.keySearch'] + : ['title', 'description'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedTimeline(request, options); +}; + +export const persistFavorite = async ( + request: FrameworkRequest, + timelineId: string | null +): Promise<ResponseFavoriteTimeline> => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const fullName = request.user?.full_name ?? ''; + try { + let timeline: SavedTimeline = {}; + if (timelineId != null) { + const { + eventIdToNoteIds, + notes, + noteIds, + pinnedEventIds, + pinnedEventsSaveObject, + savedObjectId, + version, + ...savedTimeline + } = await getBasicSavedTimeline(request, timelineId); + timelineId = savedObjectId; // eslint-disable-line no-param-reassign + timeline = savedTimeline; + } + + const userFavoriteTimeline = { + keySearch: userName != null ? convertStringToBase64(userName) : null, + favoriteDate: new Date().valueOf(), + fullName, + userName, + }; + if (timeline.favorite != null) { + const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( + user => user.userName === userName + ); + + timeline.favorite = + alreadyExistsTimelineFavoriteByUser > -1 + ? [ + ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), + ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), + ] + : [...timeline.favorite, userFavoriteTimeline]; + } else if (timeline.favorite == null) { + timeline.favorite = [userFavoriteTimeline]; + } + + const persistResponse = await persistTimeline(request, timelineId, null, timeline); + return { + savedObjectId: persistResponse.timeline.savedObjectId, + version: persistResponse.timeline.version, + favorite: + persistResponse.timeline.favorite != null + ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) + : [], + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { return { - savedObjectId: persistResponse.timeline.savedObjectId, - version: persistResponse.timeline.version, - favorite: - persistResponse.timeline.favorite != null - ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) - : [], + savedObjectId: '', + version: '', + favorite: [], + code: 403, + message: err.message, }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - return { - savedObjectId: '', - version: '', - favorite: [], - code: 403, - message: err.message, - }; - } - throw err; } + throw err; } +}; - public async persistTimeline( - request: FrameworkRequest, - timelineId: string | null, - version: string | null, - timeline: SavedTimeline - ): Promise<ResponseTimeline> { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (timelineId == null) { - // Create new timeline - const newTimeline = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(timelineId, timeline, request.user) - ) - ); - return { - code: 200, - message: 'success', - timeline: newTimeline, - }; - } - // Update Timeline - await savedObjectsClient.update( - timelineSavedObjectType, - timelineId, - pickSavedTimeline(timelineId, timeline, request.user), - { - version: version || undefined, - } +export const persistTimeline = async ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline +): Promise<ResponseTimeline> => { + const savedObjectsClient = request.context.core.savedObjects.client; + try { + if (timelineId == null) { + // Create new timeline + const newTimeline = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(timelineId, timeline, request.user) + ) ); - return { code: 200, message: 'success', - timeline: await this.getSavedTimeline(request, timelineId), + timeline: newTimeline, }; - } catch (err) { - if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { - return { - code: 409, - message: err.message, - timeline: await this.getSavedTimeline(request, timelineId), - }; - } else if (getOr(null, 'output.statusCode', err) === 403) { - const timelineToReturn: TimelineResult = { - ...timeline, - savedObjectId: '', - version: '', - }; - return { - code: 403, - message: err.message, - timeline: timelineToReturn, - }; + } + // Update Timeline + await savedObjectsClient.update( + timelineSavedObjectType, + timelineId, + pickSavedTimeline(timelineId, timeline, request.user), + { + version: version || undefined, } - throw err; + ); + + return { + code: 200, + message: 'success', + timeline: await getSavedTimeline(request, timelineId), + }; + } catch (err) { + if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { + return { + code: 409, + message: err.message, + timeline: await getSavedTimeline(request, timelineId), + }; + } else if (getOr(null, 'output.statusCode', err) === 403) { + const timelineToReturn: TimelineResult = { + ...timeline, + savedObjectId: '', + version: '', + }; + return { + code: 403, + message: err.message, + timeline: timelineToReturn, + }; } + throw err; } +}; - public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - timelineIds.map(timelineId => - Promise.all([ - savedObjectsClient.delete(timelineSavedObjectType, timelineId), - this.note.deleteNoteByTimelineId(request, timelineId), - this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), - ]) - ) - ); - } +export const deleteTimeline = async (request: FrameworkRequest, timelineIds: string[]) => { + const savedObjectsClient = request.context.core.savedObjects.client; - private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + await Promise.all( + timelineIds.map(timelineId => + Promise.all([ + savedObjectsClient.delete(timelineSavedObjectType, timelineId), + note.deleteNoteByTimelineId(request, timelineId), + pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), + ]) + ) + ); +}; - return convertSavedObjectToSavedTimeline(savedObject); - } +const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + + return convertSavedObjectToSavedTimeline(savedObject); +}; - private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; +const getSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - const timelineWithNotesAndPinnedEvents = await Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), - Promise.resolve(timelineSaveObject), - ]); + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + const timelineWithNotesAndPinnedEvents = await Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); - const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; + const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; - return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); + return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); +}; + +const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const savedObjectsClient = request.context.core.savedObjects.client; + if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { + options.search = `${options.search != null ? options.search : ''} ${ + userName != null ? convertStringToBase64(userName) : null + }`; } - private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { - options.search = `${options.search != null ? options.search : ''} ${ - userName != null ? convertStringToBase64(userName) : null - }`; - } + const savedObjects = await savedObjectsClient.find(options); - const savedObjects = await savedObjectsClient.find(options); - - const timelinesWithNotesAndPinnedEvents = await Promise.all( - savedObjects.saved_objects.map(async savedObject => { - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - return Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId( - request, - timelineSaveObject.savedObjectId - ), - Promise.resolve(timelineSaveObject), - ]); - }) - ); + const timelinesWithNotesAndPinnedEvents = await Promise.all( + savedObjects.saved_objects.map(async savedObject => { + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + return Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); + }) + ); - return { - totalCount: savedObjects.total, - timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => - timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) - ), - }; - } -} + return { + totalCount: savedObjects.total, + timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => + timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) + ), + }; +}; export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64'); @@ -283,11 +331,9 @@ export const timelineWithReduxProperties = ( timeline.favorite != null && userName != null ? timeline.favorite.filter(fav => fav.userName === userName) : [], - eventIdToNoteIds: notes.filter(note => note.eventId != null), - noteIds: notes - .filter(note => note.eventId == null && note.noteId != null) - .map(note => note.noteId), + eventIdToNoteIds: notes.filter(n => n.eventId != null), + noteIds: notes.filter(n => n.eventId == null && n.noteId != null).map(n => n.noteId), notes, - pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId), + pinnedEventIds: pinnedEvents.map(e => e.eventId), pinnedEventsSaveObject: pinnedEvents, }); diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index b956e0f98fcb6..1cab24d0879ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -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 0bce3300591c2..0000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/types.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; -import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types'; -import { - PinnedEventToReturnSavedObjectRuntimeType, - PinnedEventSavedObject, -} from '../pinned_event/types'; -import { SavedObjectsClient } from '../../../../../../src/core/server'; - -/* - * ColumnHeader Types - */ -const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ - aggregatable: unionWithNullType(runtimeTypes.boolean), - category: unionWithNullType(runtimeTypes.string), - columnHeaderType: unionWithNullType(runtimeTypes.string), - description: unionWithNullType(runtimeTypes.string), - example: unionWithNullType(runtimeTypes.string), - indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - placeholder: unionWithNullType(runtimeTypes.string), - searchable: unionWithNullType(runtimeTypes.boolean), - type: unionWithNullType(runtimeTypes.string), -}); - -/* - * DataProvider Types - */ -const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ - field: unionWithNullType(runtimeTypes.string), - displayField: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), - displayValue: unionWithNullType(runtimeTypes.string), - operator: unionWithNullType(runtimeTypes.string), -}); - -const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), -}); - -const SavedDataProviderRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), - and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), -}); - -/* - * Filters Types - */ -const SavedFilterMetaRuntimeType = runtimeTypes.partial({ - alias: unionWithNullType(runtimeTypes.string), - controlledBy: unionWithNullType(runtimeTypes.string), - disabled: unionWithNullType(runtimeTypes.boolean), - field: unionWithNullType(runtimeTypes.string), - formattedValue: unionWithNullType(runtimeTypes.string), - index: unionWithNullType(runtimeTypes.string), - key: unionWithNullType(runtimeTypes.string), - negate: unionWithNullType(runtimeTypes.boolean), - params: unionWithNullType(runtimeTypes.string), - type: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterRuntimeType = runtimeTypes.partial({ - exists: unionWithNullType(runtimeTypes.string), - meta: unionWithNullType(SavedFilterMetaRuntimeType), - match_all: unionWithNullType(runtimeTypes.string), - missing: unionWithNullType(runtimeTypes.string), - query: unionWithNullType(runtimeTypes.string), - range: unionWithNullType(runtimeTypes.string), - script: unionWithNullType(runtimeTypes.string), -}); - -/* - * kqlQuery -> filterQuery Types - */ -const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ - kind: unionWithNullType(runtimeTypes.string), - expression: unionWithNullType(runtimeTypes.string), -}); - -const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), - serializedQuery: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), -}); - -/* - * DatePicker Range Types - */ -const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ - start: unionWithNullType(runtimeTypes.number), - end: unionWithNullType(runtimeTypes.number), -}); - -/* - * Favorite Types - */ -const SavedFavoriteRuntimeType = runtimeTypes.partial({ - keySearch: unionWithNullType(runtimeTypes.string), - favoriteDate: unionWithNullType(runtimeTypes.number), - fullName: unionWithNullType(runtimeTypes.string), - userName: unionWithNullType(runtimeTypes.string), -}); - -/* - * Sort Types - */ -const SavedSortRuntimeType = runtimeTypes.partial({ - columnId: unionWithNullType(runtimeTypes.string), - sortDirection: unionWithNullType(runtimeTypes.string), -}); - -/* - * Timeline Types - */ -export const SavedTimelineRuntimeType = runtimeTypes.partial({ - columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), - dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), - description: unionWithNullType(runtimeTypes.string), - eventType: unionWithNullType(runtimeTypes.string), - favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), - filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), - kqlMode: unionWithNullType(runtimeTypes.string), - kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), - title: unionWithNullType(runtimeTypes.string), - dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), - savedQueryId: unionWithNullType(runtimeTypes.string), - sort: unionWithNullType(SavedSortRuntimeType), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), -}); - -export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -/** - * Timeline Saved object type with metadata - */ - -export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedTimelineRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - savedObjectId: runtimeTypes.string, - }), -]); - -export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ - SavedTimelineRuntimeType, - runtimeTypes.type({ - savedObjectId: runtimeTypes.string, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - noteIds: runtimeTypes.array(runtimeTypes.string), - notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - pinnedEventIds: runtimeTypes.array(runtimeTypes.string), - pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), - }), -]); - -export interface TimelineSavedObject - extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {} - -/** - * All Timeline Saved object type with metadata - */ - -export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ - total: runtimeTypes.number, - data: TimelineSavedToReturnObjectRuntimeType, -}); - -export interface AllTimelineSavedObject - extends runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType> {} - -/** - * Import/export timelines - */ - -export type ExportTimelineSavedObjectsClient = Pick< - SavedObjectsClient, - | 'get' - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'update' - | 'bulkUpdate' ->; - -export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; -export type ExportedEventNotes = NoteSavedObject[]; - -export interface ExportedNotes { - eventNotes: ExportedEventNotes; - globalNotes: ExportedGlobalNotes; -} - -export type ExportedTimelines = TimelineSavedObject & - ExportedNotes & { - pinnedEventIds: string[]; - }; - -export interface BulkGetInput { - type: string; - id: string; -} - -export type NotesAndPinnedEventsByTimelineId = Record< - string, - { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } ->; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3988fbec05de4..dc484b75ab531 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 1c03823e85fd7..ffad86a09cee7 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 da38e8a1e1bf2..4f19bd54b01f0 100644 --- a/x-pack/plugins/siem/server/utils/typed_resolvers.ts +++ b/x-pack/plugins/siem/server/utils/typed_resolvers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as runtimeTypes from 'io-ts'; import { GraphQLResolveInfo } from 'graphql'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,6 +105,3 @@ export type ChildResolverOf<Resolver_, ParentResolver> = ResolverWithParent< Resolver_, ResultOf<ParentResolver> >; - -export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) => - runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index a70fbdb18c30b..0f6e3fc31d96d 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -12,11 +12,10 @@ import { TaskManager } from './task_manager'; import { createTaskManager } from './create_task_manager'; import { TaskManagerConfig } from './config'; import { Middleware } from './lib/middleware'; +import { setupSavedObjects } from './saved_objects'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginLegacyDependencies {} export type TaskManagerSetupContract = { - registerLegacyAPI: (legacyDependencies: PluginLegacyDependencies) => Promise<TaskManager>; + registerLegacyAPI: () => Promise<TaskManager>; } & Pick<TaskManager, 'addMiddleware' | 'registerTaskDefinitions'>; export type TaskManagerStartContract = Pick< @@ -35,12 +34,18 @@ export class TaskManagerPlugin this.currentConfig = {} as TaskManagerConfig; } - public setup(core: CoreSetup, plugins: 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 0000000000000..0ad9021cd7f39 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { TaskManagerConfig } from '../config.js'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + config: TaskManagerConfig +) { + savedObjects.registerType({ + name: 'task', + namespaceType: 'agnostic', + hidden: true, + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + mappings: mappings.task, + indexPattern: config.index, + }); +} diff --git a/x-pack/legacy/plugins/task_manager/server/mappings.json b/x-pack/plugins/task_manager/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/task_manager/server/mappings.json rename to x-pack/plugins/task_manager/server/saved_objects/mappings.json diff --git a/x-pack/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 24ceea0fe71ef..2a45a599120dd 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 591de8ace1869..81dc44f3a4cb4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2579,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": "緯度", @@ -2601,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": "作成", @@ -6370,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": "エンドポイント", @@ -8246,11 +8225,8 @@ "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", - "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名前", @@ -8326,13 +8302,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": "概要", @@ -8341,7 +8310,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集", @@ -8349,7 +8317,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示", "xpack.ingestManager.configDetails.subTabs.datasouces": "データソース", "xpack.ingestManager.configDetails.subTabs.settings": "設定", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル", @@ -8411,8 +8378,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": "キャンセル", @@ -8435,9 +8400,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", @@ -10537,10 +10500,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": "実行中", @@ -12652,7 +12614,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": "システム管理者にお問い合わせください。", @@ -14135,7 +14096,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": "「コード」", @@ -14380,7 +14340,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": "以下を経由してユーザーを受け入れました:", @@ -16147,7 +16106,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 c46f395a8c64e..e06edb45de8fa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2580,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": "纬度", @@ -2602,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": "创建", @@ -6372,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": "终端", @@ -8249,11 +8228,8 @@ "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", - "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名称", @@ -8329,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": "Fleet", "xpack.ingestManager.appNavigation.overviewLinkText": "概览", @@ -8344,7 +8313,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源", @@ -8352,7 +8320,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源", "xpack.ingestManager.configDetails.subTabs.datasouces": "数据源", "xpack.ingestManager.configDetails.subTabs.settings": "设置", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件", @@ -8414,8 +8381,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": "取消", @@ -8438,9 +8403,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", @@ -10540,10 +10503,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": "正在运行", @@ -12656,7 +12618,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": "请联系您的管理员。", @@ -14139,7 +14100,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`", @@ -14384,7 +14344,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": "已接受该用户 - 通过", @@ -16152,7 +16111,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/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 9da4f059f8967..230b896eeca7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -7,24 +7,55 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; -import { Alert, ActionType } from '../../../../types'; -import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiBetaBadge } from '@elastic/eui'; +import { Alert, ActionType, AlertTypeRegistryContract } from '../../../../types'; +import { + EuiTitle, + EuiBadge, + EuiFlexItem, + EuiSwitch, + EuiBetaBadge, + EuiButtonEmpty, +} from '@elastic/eui'; import { times, random } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; +import { coreMock } from 'src/core/public/mocks'; +const mockes = coreMock.createSetup(); jest.mock('../../../app_context', () => ({ useAppDependencies: jest.fn(() => ({ http: jest.fn(), - legacy: { - capabilities: { - get: jest.fn(() => ({})), - }, + capabilities: { + get: jest.fn(() => ({})), }, + actionTypeRegistry: jest.fn(), + alertTypeRegistry: jest.fn(() => { + const mocked: jest.Mocked<AlertTypeRegistryContract> = { + has: jest.fn(), + register: jest.fn(), + get: jest.fn(), + list: jest.fn(), + }; + return mocked; + }), + toastNotifications: mockes.notifications.toasts, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + uiSettings: mockes.uiSettings, + dataPlugin: jest.fn(), + charts: jest.fn(), })), })); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); + jest.mock('../../../lib/capabilities', () => ({ hasSaveAlertsCapability: jest.fn(() => true), })); @@ -232,6 +263,28 @@ describe('alert_details', () => { ).containsMatchingElement(<ViewInApp alert={alert} />) ).toBeTruthy(); }); + + it('links to the Edit flyout', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiButtonEmpty) + .find('[data-test-subj="openEditAlertFlyoutButton"]') + .first() + .exists() + ).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 5bfcf9fd2d4e6..318dd28d92da1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { indexBy } from 'lodash'; +import { useHistory } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, @@ -21,6 +22,7 @@ import { EuiCallOut, EuiSpacer, EuiBetaBadge, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -34,6 +36,9 @@ import { import { AlertInstancesRouteWithApi } from './alert_instances_route'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; +import { AlertEdit } from '../../alert_form'; +import { AlertsContextProvider } from '../../../context/alerts_context'; +import { routeToAlertDetails } from '../../../constants'; type AlertDetailsProps = { alert: Alert; @@ -52,7 +57,18 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ muteAlert, requestRefresh, }) => { - const { capabilities } = useAppDependencies(); + const history = useHistory(); + const { + http, + toastNotifications, + capabilities, + alertTypeRegistry, + actionTypeRegistry, + uiSettings, + docLinks, + charts, + dataPlugin, + } = useAppDependencies(); const canSave = hasSaveAlertsCapability(capabilities); @@ -61,6 +77,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled); const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); + + const setAlert = async () => { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }; return ( <EuiPage> @@ -90,6 +111,42 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ </EuiPageContentHeaderSection> <EuiPageContentHeaderSection> <EuiFlexGroup responsive={false} gutterSize="xs"> + {canSave ? ( + <EuiFlexItem grow={false}> + <Fragment> + {' '} + <EuiButtonEmpty + data-test-subj="openEditAlertFlyoutButton" + iconType="pencil" + onClick={() => setEditFlyoutVisibility(true)} + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel" + defaultMessage="Edit" + /> + </EuiButtonEmpty> + <AlertsContextProvider + value={{ + http, + actionTypeRegistry, + alertTypeRegistry, + toastNotifications, + uiSettings, + docLinks, + charts, + dataFieldsFormats: dataPlugin.fieldFormats, + reloadAlerts: setAlert, + }} + > + <AlertEdit + initialAlert={alert} + editFlyoutVisible={editFlyoutVisible} + setEditFlyoutVisibility={setEditFlyoutVisibility} + /> + </AlertsContextProvider> + </Fragment> + </EuiFlexItem> + ) : null} <EuiFlexItem grow={false}> <ViewInApp alert={alert} /> </EuiFlexItem> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index 498ecffe9b947..b9d08abae1684 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 bdca506cc7338..0cdf1ca05feac 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -25,6 +25,8 @@ import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices'; import { registerTelemetryRoutes } from './routes/telemetry'; +import { telemetrySavedObjectType, reindexOperationSavedObjectType } from './saved_object_types'; + import { RouteDependencies } from './types'; interface PluginsSetup { @@ -57,11 +59,14 @@ export class UpgradeAssistantServerPlugin implements Plugin { } setup( - { http, getStartServices, capabilities }: CoreSetup, + { http, getStartServices, capabilities, savedObjects }: CoreSetup, { usageCollection, cloud, licensing }: PluginsSetup ) { this.licensing = licensing; + savedObjects.registerType(reindexOperationSavedObjectType); + savedObjects.registerType(telemetrySavedObjectType); + const router = http.createRouter(); const dependencies: RouteDependencies = { @@ -85,8 +90,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerTelemetryRoutes(dependencies); if (usageCollection) { - getStartServices().then(([{ savedObjects, elasticsearch }]) => { - registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); + getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + registerUpgradeAssistantUsageCollector({ + elasticsearch, + usageCollection, + savedObjects: savedObjectsService, + }); }); } } diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts new file mode 100644 index 0000000000000..dee0a74d8994b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { reindexOperationSavedObjectType } from './reindex_operation_saved_object_type'; +export { telemetrySavedObjectType } from './telemetry_saved_object_type'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts new file mode 100644 index 0000000000000..ba661fbeceb26 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from 'src/core/server'; + +import { REINDEX_OP_TYPE } from '../../common/types'; + +export const reindexOperationSavedObjectType: SavedObjectsType = { + name: REINDEX_OP_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + reindexTaskId: { + type: 'keyword', + }, + indexName: { + type: 'keyword', + }, + newIndexName: { + type: 'keyword', + }, + status: { + type: 'integer', + }, + locked: { + type: 'date', + }, + lastCompletedStep: { + type: 'integer', + }, + errorMessage: { + type: 'keyword', + }, + reindexTaskPercComplete: { + type: 'float', + }, + runningReindexCount: { + type: 'integer', + }, + reindexOptions: { + properties: { + openAndClose: { + type: 'boolean', + }, + queueSettings: { + properties: { + queuedAt: { + type: 'long', + }, + startedAt: { + type: 'long', + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts new file mode 100644 index 0000000000000..b1321e634c0f1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from 'src/core/server'; + +import { UPGRADE_ASSISTANT_TYPE } from '../../common/types'; + +export const telemetrySavedObjectType: SavedObjectsType = { + name: UPGRADE_ASSISTANT_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + ui_open: { + properties: { + overview: { + type: 'long', + null_value: 0, + }, + cluster: { + type: 'long', + null_value: 0, + }, + indices: { + type: 'long', + null_value: 0, + }, + }, + }, + ui_reindex: { + properties: { + close: { + type: 'long', + null_value: 0, + }, + open: { + type: 'long', + null_value: 0, + }, + start: { + type: 'long', + null_value: 0, + }, + stop: { + type: 'long', + null_value: 0, + }, + }, + }, + features: { + properties: { + deprecation_logging: { + properties: { + enabled: { + type: 'boolean', + null_value: true, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md similarity index 100% rename from x-pack/legacy/plugins/uptime/README.md rename to x-pack/plugins/uptime/README.md diff --git a/x-pack/legacy/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/alerts.ts rename to x-pack/plugins/uptime/common/constants/alerts.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/capabilities.ts b/x-pack/plugins/uptime/common/constants/capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/capabilities.ts rename to x-pack/plugins/uptime/common/constants/capabilities.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts b/x-pack/plugins/uptime/common/constants/chart_format_limits.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts rename to x-pack/plugins/uptime/common/constants/chart_format_limits.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts b/x-pack/plugins/uptime/common/constants/client_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts rename to x-pack/plugins/uptime/common/constants/client_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/plugins/uptime/common/constants/context_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts rename to x-pack/plugins/uptime/common/constants/context_defaults.ts diff --git a/x-pack/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 0000000000000..6064524872a0a --- /dev/null +++ b/x-pack/plugins/uptime/common/constants/plugin.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PLUGIN = { + APP_ROOT_ID: 'react-uptime-root', + DESCRIPTION: i18n.translate('xpack.uptime.pluginDescription', { + defaultMessage: 'Uptime monitoring', + description: 'The description text that will appear in the feature catalogue.', + }), + ID: 'uptime', + LOCAL_STORAGE_KEY: 'xpack.uptime', + NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + ROUTER_BASE_NAME: '/app/uptime#', + TITLE: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { + defaultMessage: 'Uptime', + }), +}; diff --git a/x-pack/legacy/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts similarity index 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 0000000000000..e80471bf8b56f --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './alerts'; +export * from './certs'; +export * from './common'; +export * from './dynamic_settings'; +export * from './monitor'; +export * from './overview_filters'; +export * from './ping'; +export * from './snapshot'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/details.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/state.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts b/x-pack/plugins/uptime/common/runtime_types/ping/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts similarity index 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 0000000000000..71ccd54dd3cdc --- /dev/null +++ b/x-pack/plugins/uptime/common/types/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** Represents the average monitor duration ms at a point in time. */ +export interface MonitorDurationAveragePoint { + /** The timeseries value for this point. */ + x: number; + /** The average duration ms for the monitor. */ + y?: number | null; +} + +export interface LocationDurationLine { + name: string; + + line: MonitorDurationAveragePoint[]; +} + +/** The data used to populate the monitor charts. */ +export interface MonitorDurationResult { + /** The average values for the monitor duration. */ + locationDurationLines: LocationDurationLine[]; +} + +export interface MonitorIdParam { + monitorId: string; +} diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 6ec8a755ebea0..ce8b64ce07254 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,8 +2,16 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting", "features", "licensing", "usageCollection"], + "optionalPlugins": ["capabilities", "data", "home"], + "requiredPlugins": [ + "alerting", + "embeddable", + "features", + "licensing", + "triggers_actions_ui", + "usageCollection" + ], "server": true, - "ui": false, + "ui": true, "version": "8.0.0" } diff --git a/x-pack/legacy/plugins/uptime/public/app.ts b/x-pack/plugins/uptime/public/app.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/app.ts rename to x-pack/plugins/uptime/public/app.ts diff --git a/x-pack/plugins/uptime/public/apps/index.ts b/x-pack/plugins/uptime/public/apps/index.ts new file mode 100644 index 0000000000000..65b80d08d4f20 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UptimePlugin } from './plugin'; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts new file mode 100644 index 0000000000000..719dac022dada --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LegacyCoreStart, AppMountParameters } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { UMFrontendLibs } from '../lib/lib'; +import { PLUGIN } from '../../common/constants'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; + +export interface StartObject { + core: LegacyCoreStart; + plugins: any; +} + +export interface ClientPluginsSetup { + data: DataPublicPluginSetup; + home: HomePublicPluginSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface ClientPluginsStart { + embeddable: EmbeddableStart; +} + +export class UptimePlugin implements Plugin<void, void, ClientPluginsSetup, ClientPluginsStart> { + constructor(_context: PluginInitializerContext) {} + + public async setup( + core: CoreSetup<ClientPluginsStart, unknown>, + plugins: ClientPluginsSetup + ): Promise<void> { + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + description: PLUGIN.DESCRIPTION, + icon: 'uptimeApp', + path: '/app/uptime#/', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + + core.application.register({ + appRoute: '/app/uptime#/', + id: PLUGIN.ID, + euiIconType: 'uptimeApp', + order: 8900, + title: PLUGIN.TITLE, + async mount(params: AppMountParameters) { + const [coreStart, corePlugins] = await core.getStartServices(); + const { element } = params; + const libs: UMFrontendLibs = { + framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins), + }; + libs.framework.render(element); + return () => {}; + }, + }); + } + + public start(_start: CoreStart, _plugins: {}): void {} + + public stop(): void {} +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/template.html b/x-pack/plugins/uptime/public/apps/template.html similarity index 100% rename from x-pack/legacy/plugins/uptime/public/apps/template.html rename to x-pack/plugins/uptime/public/apps/template.html diff --git a/x-pack/legacy/plugins/uptime/public/badge.ts b/x-pack/plugins/uptime/public/badge.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/badge.ts rename to x-pack/plugins/uptime/public/badge.ts diff --git a/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts b/x-pack/plugins/uptime/public/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/breadcrumbs.ts rename to x-pack/plugins/uptime/public/breadcrumbs.ts diff --git a/x-pack/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 a31a143b71fd2..ceb1e700f293e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx @@ -9,11 +9,11 @@ import moment from 'moment'; import { AnnotationTooltipFormatter, RectAnnotation } from '@elastic/charts'; import { RectAnnotationDatum } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; import { AnnotationTooltip } from './annotation_tooltip'; -import { ANOMALY_SEVERITY } from '../../../../../../../plugins/ml/common/constants/anomalies'; +import { ANOMALY_SEVERITY } from '../../../../../../plugins/ml/common/constants/anomalies'; import { getSeverityColor, getSeverityType, -} from '../../../../../../../plugins/ml/common/util/anomaly_utils'; +} from '../../../../../../plugins/ml/common/util/anomaly_utils'; interface Props { anomalies: any; diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts b/x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts rename to x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts b/x-pack/plugins/uptime/public/components/common/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts rename to x-pack/plugins/uptime/public/components/common/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx rename to x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx rename to x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts b/x-pack/plugins/uptime/public/components/common/higher_order/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts rename to x-pack/plugins/uptime/public/components/common/higher_order/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx b/x-pack/plugins/uptime/public/components/common/location_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx rename to x-pack/plugins/uptime/public/components/common/location_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx rename to x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/index.ts rename to x-pack/plugins/uptime/public/components/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx new file mode 100644 index 0000000000000..06cdb07bd8bcd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useContext, useRef } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; +import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../../legacy/plugins/maps/public'; +import * as i18n from './translations'; +import { Location } from '../../../../../common/runtime_types'; +import { getLayerList } from './map_config'; +import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../contexts'; +import { + isErrorEmbeddable, + ViewMode, + ErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/public'; + +export interface EmbeddedMapProps { + upPoints: LocationPoint[]; + downPoints: LocationPoint[]; +} + +export type LocationPoint = Required<Location>; + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { + const { colors } = useContext(UptimeThemeContext); + const [embeddable, setEmbeddable] = useState<MapEmbeddable | ErrorEmbeddable | undefined>(); + const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null); + const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + filters: [], + hidePanelTitles: true, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + hideFilterActions: true, + // Zoom Lat/Lon values are set to make sure map is in center in the panel + // It wil also omit Greenland/Antarctica etc + mapCenter: { + lon: 11, + lat: 20, + zoom: 0, + }, + disableInteractive: true, + disableTooltipControl: true, + hideToolbarOverlay: true, + hideLayerControl: true, + hideViewControl: true, + }; + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: i18n.MAP_TITLE, + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); + } + + setEmbeddable(embeddableObject); + } + setupEmbeddable(); + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // update map layers based on points + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); + } + }, [upPoints, downPoints, embeddable, colors]); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + return ( + <EmbeddedPanel> + <div + data-test-subj="xpack.uptime.locationMap.embeddedPanel" + className="embPanel__content" + ref={embeddableRoot} + /> + </EmbeddedPanel> + ); +}); + +EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx index c0b02181dcce1..31cdcfac9feef 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx @@ -9,7 +9,7 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MLFlyoutView } from '../ml_flyout'; import { UptimeSettingsContext } from '../../../../contexts'; import { CLIENT_DEFAULTS } from '../../../../../common/constants'; -import { License } from '../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../../plugins/licensing/common/license'; const expiredLicense = new License({ signature: 'test signature', diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts b/x-pack/plugins/uptime/public/components/monitor/ml/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts rename to x-pack/plugins/uptime/public/components/monitor/ml/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index c3e8579ca4837..6eec30d405f76 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -19,7 +19,7 @@ import * as labels from './translations'; import { useKibana, KibanaReactNotifications, -} from '../../../../../../../../src/plugins/kibana_react/public'; +} from '../../../../../../../src/plugins/kibana_react/public'; import { MLFlyoutView } from './ml_flyout'; import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index 4963a901f0ecc..7f19885c15406 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -18,9 +18,9 @@ import { ConfirmJobDeletion } from './confirm_delete'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; import * as labels from './translations'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../../plugins/ml/common/types/data_recognizer'; import { useMonitorId } from '../../../hooks'; export const MLIntegrationComponent = () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 7e39b977f1271..52d4f620f84b3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -20,7 +20,7 @@ import { } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../ml/common/types/data_recognizer'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../common/types'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_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 04dfe4b3e3509..92fc015a276d3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } f import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { setAlertFlyoutVisible: (value: boolean) => void; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx similarity index 83% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx index 09e6dc72b7f98..262e1552e3660 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { AlertsContextProvider } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { AlertsContextProvider } from '../../../../../../plugins/triggers_actions_ui/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const UptimeAlertsContextProvider: React.FC = ({ children }) => { const { diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 13705e7d19293..9b1d3a73dc661 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AlertAdd } from '../../../../../../../plugins/triggers_actions_ui/public'; +import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx similarity index 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 651103a34bf21..d38f203739cea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx @@ -10,7 +10,7 @@ import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; import { DataOrIndexMissing } from './data_or_index_missing'; import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index 1135b969018a1..aa4040e319e0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateErrorProps { errors: IHttpFetchError[]; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts b/x-pack/plugins/uptime/public/components/overview/empty_state/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts rename to x-pack/plugins/uptime/public/components/overview/empty_state/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/index.ts b/x-pack/plugins/uptime/public/components/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/index.ts rename to x-pack/plugins/uptime/public/components/overview/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts diff --git a/x-pack/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 63aceed2be636..792fff4c7cdca 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 2b5ad9b59e39f..936eae04ffa64 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 7fbabe71bcdb5..ac6832050b9d3 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 18e2e2437e147..7e9536689470e 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 5bfe6ff0c5b4f..6fb880e28c734 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -9,7 +9,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { getMonitorList } from '../../../state/actions'; import { FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; import { monitorListSelector } from '../../../state/selectors'; -import { MonitorListComponent } from './index'; +import { MonitorListComponent } from './monitor_list'; export interface MonitorListProps { filters?: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx similarity index 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 0000000000000..243a25c26901a --- /dev/null +++ b/x-pack/plugins/uptime/public/contexts/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UptimeRefreshContext, UptimeRefreshContextProvider } from './uptime_refresh_context'; +export { + UptimeSettingsContextValues, + UptimeSettingsContext, + UptimeSettingsContextProvider, +} from './uptime_settings_context'; +export { UptimeThemeContextProvider, UptimeThemeContext } from './uptime_theme_context'; +export { + UptimeStartupPluginsContext, + UptimeStartupPluginsContextProvider, +} from './uptime_startup_plugins_context'; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx index 137846de103b4..4fabf3f2ed497 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -9,7 +9,7 @@ import { UptimeAppProps } from '../uptime_app'; import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants'; import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; import { useGetUrlParams } from '../hooks'; -import { ILicense } from '../../../../../plugins/licensing/common/types'; +import { ILicense } from '../../../../plugins/licensing/common/types'; export interface UptimeSettingsContextValues { basePath: string; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx new file mode 100644 index 0000000000000..e516ff44aa12a --- /dev/null +++ b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext } from 'react'; +import { ClientPluginsStart } from '../apps/plugin'; + +export const UptimeStartupPluginsContext = createContext<Partial<ClientPluginsStart>>({}); + +export const UptimeStartupPluginsContextProvider: React.FC<Partial<ClientPluginsStart>> = ({ + children, + ...props +}) => <UptimeStartupPluginsContext.Provider value={{ ...props }} children={children} />; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap b/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap rename to x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx index 1ce00fe7ce3af..306919015fcb1 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { mountWithRouter } from '../../lib'; import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx diff --git a/x-pack/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 ab4d6f75849e8..492d2bab5bb80 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -5,7 +5,7 @@ */ import { combineFiltersAndUserSearch, stringifyKueries } from '../lib/helper'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; const getKueryString = (urlFilters: string): string => { let kueryString = ''; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts rename to x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts index d1cc8e1897386..182c6b0114128 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; import { UptimeUrlParams } from '../lib/helper'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { useUrlParams } from '.'; export const makeBaseBreadcrumb = (params?: UptimeUrlParams): ChromeBreadcrumb => { diff --git a/x-pack/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 0000000000000..48cf2c90ad07b --- /dev/null +++ b/x-pack/plugins/uptime/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { UptimePlugin } from './apps'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new UptimePlugin(initializerContext); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts b/x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts rename to x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx similarity index 86% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx rename to x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 71c73bf5ba5d4..f7f9e056f41af 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -9,7 +9,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; import { i18n as i18nFormatter } from '@kbn/i18n'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; import { alertTypeInitializers } from '../../alert_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -20,10 +19,12 @@ import { DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; import { UMFrameworkAdapter } from '../../lib'; +import { ClientPluginsStart, ClientPluginsSetup } from '../../../apps/plugin'; export const getKibanaFrameworkAdapter = ( core: CoreStart, - plugins: PluginsSetup + plugins: ClientPluginsSetup, + startPlugins: ClientPluginsStart ): UMFrameworkAdapter => { const { application: { capabilities }, @@ -35,14 +36,15 @@ export const getKibanaFrameworkAdapter = ( const { data: { autocomplete }, - // TODO: after NP migration we can likely fix this typing problem - // @ts-ignore we don't control this type triggers_actions_ui, } = plugins; - alertTypeInitializers.forEach(init => - triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })) - ); + alertTypeInitializers.forEach(init => { + const alertInitializer = init({ autocomplete }); + if (!triggers_actions_ui.alertTypeRegistry.has(alertInitializer.id)) { + triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })); + } + }); let breadcrumbs: ChromeBreadcrumb[] = []; core.chrome.getBreadcrumbs$().subscribe((nextBreadcrumbs?: ChromeBreadcrumb[]) => { @@ -68,6 +70,7 @@ export const getKibanaFrameworkAdapter = ( isLogsAvailable: logs, kibanaBreadcrumbs: breadcrumbs, plugins, + startPlugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts rename to x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts new file mode 100644 index 0000000000000..f7ab254ffe675 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { initMonitorStatusAlertType } from './monitor_status'; + +export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; + +export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType]; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx rename to x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 0624d20b197c0..e7695fb1cbb56 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -8,17 +8,12 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import React from 'react'; import DateMath from '@elastic/datemath'; import { isRight } from 'fp-ts/lib/Either'; -import { - AlertTypeModel, - ValidationResult, - // TODO: this typing issue should be resolved after NP migration - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers'; -export const validate = (alertParams: any): ValidationResult => { +export const validate = (alertParams: any) => { const errors: Record<string, any> = {}; const decoded = StatusCheckExecutorParamsType.decode(alertParams); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts b/x-pack/plugins/uptime/public/lib/helper/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts b/x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts b/x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts rename to x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts b/x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts rename to x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts b/x-pack/plugins/uptime/public/lib/helper/get_title.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts rename to x-pack/plugins/uptime/public/lib/helper/get_title.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx rename to x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts b/x-pack/plugins/uptime/public/lib/helper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/index.ts rename to x-pack/plugins/uptime/public/lib/helper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts b/x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts rename to x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts diff --git a/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts new file mode 100644 index 0000000000000..a5c2168378089 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseUrlInt } from '../parse_url_int'; + +describe('parseUrlInt', () => { + it('parses a number', () => { + const result = parseUrlInt('23', 50); + expect(result).toBe(23); + }); + + it('returns default value for empty string', () => { + const result = parseUrlInt('', 50); + expect(result).toBe(50); + }); + + it('returns default value for non-numeric string', () => { + const result = parseUrlInt('abc', 50); + expect(result).toBe(50); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts similarity index 87% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts index b1a4d0a2aba0d..0e5363527b516 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: add a comment explaining the purpose of this function export const parseUrlInt = (value: string | undefined, defaultValue: number): number => { const parsed = parseInt(value || '', 10); return isNaN(parsed) ? defaultValue : parsed; diff --git a/x-pack/legacy/plugins/uptime/public/lib/index.ts b/x-pack/plugins/uptime/public/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/index.ts rename to x-pack/plugins/uptime/public/lib/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/lib.ts b/x-pack/plugins/uptime/public/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/lib.ts rename to x-pack/plugins/uptime/public/lib/lib.ts diff --git a/x-pack/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 4495be9b24dc1..8a309db75acd2 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 adc36efa6f7db..fefd804cbfabf 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -10,11 +10,11 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; -import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; -import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; +import { DataPublicPluginSetup, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { useUpdateKueryString } from '../hooks'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/plugins/uptime/public/pages/page_header.tsx similarity index 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 0000000000000..52096a49435d7 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch, useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; +import { Link } from 'react-router-dom'; +import { selectDynamicSettings } from '../state/selectors'; +import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; +import { DynamicSettings } from '../../common/runtime_types'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { OVERVIEW_ROUTE } from '../../common/constants'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { IndicesForm } from '../components/settings/indices_form'; +import { + CertificateExpirationForm, + OnFieldChangeType, +} from '../components/settings/certificate_form'; +import * as Translations from './translations'; + +interface SettingsPageFieldErrors { + heartbeatIndices: 'May not be blank' | ''; + certificatesThresholds: { + expirationThresholdError: string | null; + ageThresholdError: string | null; + } | null; +} + +export interface SettingsFormProps { + loading: boolean; + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: SettingsPageFieldErrors | null; + isDisabled: boolean; +} + +const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { + if (formFields) { + const blankStr = 'May not be blank'; + const { certThresholds: certificatesThresholds, heartbeatIndices } = formFields; + const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; + const expirationThresholdError = certificatesThresholds?.expiration ? null : blankStr; + const ageThresholdError = certificatesThresholds?.age ? null : blankStr; + return { + heartbeatIndices: heartbeatIndErr, + certificatesThresholds: + expirationThresholdError || ageThresholdError + ? { + expirationThresholdError, + ageThresholdError, + } + : null, + }; + } + return null; +}; + +export const SettingsPage = () => { + const dss = useSelector(selectDynamicSettings); + + useBreadcrumbs([{ text: Translations.settings.breadcrumbText }]); + + useUptimeTelemetry(UptimePage.Settings); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + const [formFields, setFormFields] = useState<DynamicSettings | null>( + dss.settings ? { ...dss.settings } : null + ); + + if (!dss.loadError && formFields === null && dss.settings) { + setFormFields(Object.assign({}, { ...dss.settings })); + } + + const fieldErrors = getFieldErrors(formFields); + + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); + + const onChangeFormField: OnFieldChangeType = changedField => { + if (formFields) { + setFormFields({ + heartbeatIndices: changedField.heartbeatIndices ?? formFields.heartbeatIndices, + certThresholds: Object.assign( + {}, + formFields.certThresholds, + changedField?.certThresholds ?? null + ), + }); + } + }; + + const onApply = (event: React.FormEvent) => { + event.preventDefault(); + if (formFields) { + dispatch(setDynamicSettings(formFields)); + } + }; + + const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); + + const isFormDirty = !isEqual(dss.settings, formFields); + const canEdit: boolean = + !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; + const isFormDisabled = dss.loading || !canEdit; + + const cannotEditNotice = canEdit ? null : ( + <> + <EuiCallOut title={Translations.settings.editNoticeTitle}> + {Translations.settings.editNoticeText} + </EuiCallOut> + <EuiSpacer size="s" /> + </> + ); + + return ( + <> + <Link to={OVERVIEW_ROUTE} data-test-subj="uptimeSettingsToOverviewLink"> + <EuiButtonEmpty size="s" color="primary" iconType="arrowLeft"> + {Translations.settings.returnToOverviewLinkLabel} + </EuiButtonEmpty> + </Link> + <EuiSpacer size="s" /> + <EuiPanel> + <EuiFlexGroup> + <EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <form onSubmit={onApply}> + <EuiForm> + <IndicesForm + loading={dss.loading} + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> + <CertificateExpirationForm + loading={dss.loading} + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> + + <EuiSpacer size="m" /> + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="discardSettingsButton" + isDisabled={!isFormDirty || isFormDisabled} + onClick={() => { + resetForm(); + }} + > + <FormattedMessage + id="xpack.uptime.sourceConfiguration.discardSettingsButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="apply-settings-button" + type="submit" + color="primary" + isDisabled={!isFormDirty || !isFormValid || isFormDisabled} + fill + > + <FormattedMessage + id="xpack.uptime.sourceConfiguration.applySettingsButtonLabel" + defaultMessage="Apply changes" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </form> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </> + ); +}; diff --git a/x-pack/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 b5e20ef8a70a9..eb0587c0417a2 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 0000000000000..441a3cefdf204 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { createAsyncAction } from './utils'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { AnomaliesTableRecord } from '../../../../../plugins/ml/common/types/anomalies'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from './types'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; + +export const resetMLState = createAction('RESET_ML_STATE'); + +export const getExistingMLJobAction = createAsyncAction<MonitorIdParam, JobExistResult>( + 'GET_EXISTING_ML_JOB' +); + +export const createMLJobAction = createAsyncAction< + MonitorIdParam & HeartbeatIndicesParam, + CreateMLJobSuccess | null +>('CREATE_ML_JOB'); + +export const getMLCapabilitiesAction = createAsyncAction<any, MlCapabilitiesResponse>( + 'GET_ML_CAPABILITIES' +); + +export const deleteMLJobAction = createAsyncAction<MonitorIdParam, DeleteJobResults>( + 'DELETE_ML_JOB' +); + +export interface AnomalyRecordsParams { + dateStart: number; + dateEnd: number; + listOfMonitorIds: string[]; + anomalyThreshold?: number; +} + +export interface AnomalyRecords { + anomalies: AnomaliesTableRecord[]; + interval: string; +} + +export const getAnomalyRecordsAction = createAsyncAction<AnomalyRecordsParams, AnomalyRecords>( + 'GET_ANOMALY_RECORDS' +); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/plugins/uptime/public/state/actions/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts rename to x-pack/plugins/uptime/public/state/actions/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts similarity index 90% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_duration.ts index 9a2db5be60b12..524044f873687 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts @@ -7,7 +7,7 @@ import { createAction } from 'redux-actions'; import { QueryParams } from './types'; import { MonitorDurationResult } from '../../../common/types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; type MonitorQueryParams = QueryParams & { monitorId: string }; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts b/x-pack/plugins/uptime/public/state/actions/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/plugins/uptime/public/state/actions/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts b/x-pack/plugins/uptime/public/state/actions/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts rename to x-pack/plugins/uptime/public/state/actions/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts b/x-pack/plugins/uptime/public/state/actions/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ping.ts rename to x-pack/plugins/uptime/public/state/actions/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/plugins/uptime/public/state/actions/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts rename to x-pack/plugins/uptime/public/state/actions/snapshot.ts diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts new file mode 100644 index 0000000000000..dee2df77707d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export interface AsyncAction<Payload, SuccessPayload> { + get: (payload: Payload) => Action<Payload>; + success: (payload: SuccessPayload) => Action<SuccessPayload>; + fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; +} +export interface AsyncAction1<Payload, SuccessPayload> { + get: (payload?: Payload) => Action<Payload>; + success: (payload: SuccessPayload) => Action<SuccessPayload>; + fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; +} + +export interface MonitorIdParam { + monitorId: string; +} + +export interface HeartbeatIndicesParam { + heartbeatIndices: string; +} + +export interface QueryParams { + monitorId: string; + dateStart: string; + dateEnd: string; + filters?: string; + statusFilter?: string; + location?: string; +} + +export interface MonitorDetailsActionPayload { + monitorId: string; + dateStart: string; + dateEnd: string; + location?: string; +} + +export interface CreateMLJobSuccess { + count: number; + jobId: string; +} + +export interface DeleteJobResults { + [id: string]: { + [status: string]: boolean; + error?: any; + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ui.ts rename to x-pack/plugins/uptime/public/state/actions/ui.ts diff --git a/x-pack/plugins/uptime/public/state/actions/utils.ts b/x-pack/plugins/uptime/public/state/actions/utils.ts new file mode 100644 index 0000000000000..8ce4cf011406b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/utils.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { AsyncAction, AsyncAction1 } from './types'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export function createAsyncAction<Payload, SuccessPayload>( + actionStr: string +): AsyncAction1<Payload, SuccessPayload>; +export function createAsyncAction<Payload, SuccessPayload>( + actionStr: string +): AsyncAction<Payload, SuccessPayload> { + return { + get: createAction<Payload>(actionStr), + success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`), + fail: createAction<IHttpFetchError>(`${actionStr}_FAIL`), + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap rename to x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts new file mode 100644 index 0000000000000..838e5b8246b4b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMLJobId } from '../ml_anomaly'; + +describe('ML Anomaly API', () => { + it('it generates a lowercase job id', async () => { + const monitorId = 'ABC1334haa'; + + const jobId = getMLJobId(monitorId); + + expect(jobId).toEqual(jobId.toLowerCase()); + }); + + it('should truncate long monitor IDs', () => { + const longAndWeirdMonitorId = + 'https://auto-mmmmxhhhhhccclongAndWeirdMonitorId123yyyyyrereauto-xcmpa-1345555454646'; + + expect(getMLJobId(longAndWeirdMonitorId)).toHaveLength(64); + }); + + it('should remove special characters and replace them with underscore', () => { + const monIdSpecialChars = '/ ? , " < > | * a'; + + const jobId = getMLJobId(monIdSpecialChars); + + const format = /[/?,"<>|*]+/; + + expect(format.test(jobId)).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts similarity index 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 0000000000000..c4ecb769abefc --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { apiService } from './utils'; +import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; +import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from '../actions/types'; +import { DataRecognizerConfigResponse } from '../../../../../plugins/ml/common/types/modules'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; + +const getJobPrefix = (monitorId: string) => { + // ML App doesn't support upper case characters in job name + // Also Spaces and the characters / ? , " < > | * are not allowed + // so we will replace all special chars with _ + + const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); + + // ML Job ID can't be greater than 64 length, so will be substring it, and hope + // At such big length, there is minimum chance of having duplicate monitor id + // Subtracting ML_JOB_ID constant as well + const postfix = '_' + ML_JOB_ID; + + if ((prefix + postfix).length > 64) { + return prefix.substring(0, 64 - postfix.length) + '_'; + } + return prefix + '_'; +}; + +export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`; + +export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => { + return await apiService.get(API_URLS.ML_CAPABILITIES); +}; + +export const getExistingJobs = async (): Promise<JobExistResult> => { + return await apiService.get(API_URLS.ML_MODULE_JOBS + ML_MODULE_ID); +}; + +export const createMLJob = async ({ + monitorId, + heartbeatIndices, +}: MonitorIdParam & HeartbeatIndicesParam): Promise<CreateMLJobSuccess | null> => { + const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID; + + const data = { + prefix: `${getJobPrefix(monitorId)}`, + useDedicatedIndex: false, + startDatafeed: true, + start: moment() + .subtract(24, 'h') + .valueOf(), + indexPatternName: heartbeatIndices, + query: { + bool: { + filter: [ + { term: { 'monitor.id': monitorId } }, + { range: { 'monitor.duration.us': { gt: 0 } } }, + ], + }, + }, + }; + + const response: DataRecognizerConfigResponse = await apiService.post(url, data); + if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) { + const jobResponse = response.jobs[0]; + if (jobResponse.success) { + return { + count: 1, + jobId: jobResponse.id, + }; + } else { + const { error } = jobResponse; + throw new Error(error?.msg); + } + } else { + return null; + } +}; + +export const deleteMLJob = async ({ monitorId }: MonitorIdParam): Promise<DeleteJobResults> => { + const data = { jobIds: [getMLJobId(monitorId)] }; + + return await apiService.post(API_URLS.ML_DELETE_JOB, data); +}; + +export const fetchAnomalyRecords = async ({ + dateStart, + dateEnd, + listOfMonitorIds, + anomalyThreshold, +}: AnomalyRecordsParams): Promise<AnomalyRecords> => { + const data = { + jobIds: listOfMonitorIds.map((monitorId: string) => getMLJobId(monitorId)), + criteriaFields: [], + influencers: [], + aggregationInterval: 'auto', + threshold: anomalyThreshold ?? 25, + earliestMs: dateStart, + latestMs: dateEnd, + dateFormatTz: Intl.DateTimeFormat().resolvedOptions().timeZone, + maxRecords: 500, + maxExamples: 10, + }; + return apiService.post(API_URLS.ML_ANOMALIES_RESULT, data); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/plugins/uptime/public/state/api/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor.ts rename to x-pack/plugins/uptime/public/state/api/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts b/x-pack/plugins/uptime/public/state/api/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/api/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts b/x-pack/plugins/uptime/public/state/api/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts rename to x-pack/plugins/uptime/public/state/api/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/plugins/uptime/public/state/api/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts rename to x-pack/plugins/uptime/public/state/api/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/plugins/uptime/public/state/api/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts rename to x-pack/plugins/uptime/public/state/api/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/plugins/uptime/public/state/api/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/ping.ts rename to x-pack/plugins/uptime/public/state/api/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/plugins/uptime/public/state/api/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts rename to x-pack/plugins/uptime/public/state/api/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/plugins/uptime/public/state/api/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/types.ts rename to x-pack/plugins/uptime/public/state/api/types.ts diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts new file mode 100644 index 0000000000000..acd9bec5a74bc --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { HttpFetchQuery, HttpSetup } from '../../../../../../target/types/core/public'; + +class ApiService { + private static instance: ApiService; + private _http!: HttpSetup; + + public get http() { + return this._http; + } + + public set http(httpSetup: HttpSetup) { + this._http = httpSetup; + } + + private constructor() {} + + static getInstance(): ApiService { + if (!ApiService.instance) { + ApiService.instance = new ApiService(); + } + + return ApiService.instance; + } + + public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) { + const response = await this._http!.get(apiUrl, { query: params }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.error( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + + return response; + } + + public async post(apiUrl: string, data?: any, decodeType?: any) { + const response = await this._http!.post(apiUrl, { + method: 'POST', + body: JSON.stringify(data), + }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.warn( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + return response; + } + + public async delete(apiUrl: string) { + const response = await this._http!.delete(apiUrl); + if (response instanceof Error) { + throw response; + } + return response; + } +} + +export const apiService = ApiService.getInstance(); diff --git a/x-pack/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 b0734cb5ccabb..0aa85609fe4f0 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts @@ -6,7 +6,7 @@ import { call, put } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; /** * Factory function for a fetch effect. It expects three action creators, diff --git a/x-pack/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 bc482e2f35c45..b357f5a904ea6 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 0000000000000..61e03a9592921 --- /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 0000000000000..c81ee6875f305 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export interface AsyncInitialState<ReduceStateType> { + data: ReduceStateType | null; + loading: boolean; + error?: IHttpFetchError | null; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts rename to x-pack/plugins/uptime/public/state/reducers/ui.ts diff --git a/x-pack/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 92775a2663863..0d18f959230d1 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -10,13 +10,14 @@ import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import { I18nStart, ChromeBreadcrumb, CoreStart } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +import { ClientPluginsSetup, ClientPluginsStart } from './apps/plugin'; import { UMUpdateBadge } from './lib/lib'; import { UptimeRefreshContextProvider, UptimeSettingsContextProvider, UptimeThemeContextProvider, + UptimeStartupPluginsContextProvider, } from './contexts'; import { CommonlyUsedRange } from './components/common/uptime_date_picker'; import { store } from './state'; @@ -47,7 +48,8 @@ export interface UptimeAppProps { isInfraAvailable: boolean; isLogsAvailable: boolean; kibanaBreadcrumbs: ChromeBreadcrumb[]; - plugins: PluginsSetup; + plugins: ClientPluginsSetup; + startPlugins: ClientPluginsStart; routerBasename: string; setBadge: UMUpdateBadge; renderGlobalHelpControls(): void; @@ -66,6 +68,7 @@ const Application = (props: UptimeAppProps) => { renderGlobalHelpControls, routerBasename, setBadge, + startPlugins, } = props; useEffect(() => { @@ -87,7 +90,6 @@ const Application = (props: UptimeAppProps) => { kibanaService.core = core; - // @ts-ignore store.dispatch(setBasePath(basePath)); return ( @@ -99,17 +101,19 @@ const Application = (props: UptimeAppProps) => { <UptimeRefreshContextProvider> <UptimeSettingsContextProvider {...props}> <UptimeThemeContextProvider darkMode={darkMode}> - <UptimeAlertsContextProvider> - <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> - <main> - <UptimeAlertsFlyoutWrapper - alertTypeId="xpack.uptime.alerts.monitorStatus" - canChangeTrigger={false} - /> - <PageRouter autocomplete={plugins.data.autocomplete} /> - </main> - </EuiPage> - </UptimeAlertsContextProvider> + <UptimeStartupPluginsContextProvider {...startPlugins}> + <UptimeAlertsContextProvider> + <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> + <main> + <UptimeAlertsFlyoutWrapper + alertTypeId="xpack.uptime.alerts.monitorStatus" + canChangeTrigger={false} + /> + <PageRouter autocomplete={plugins.data.autocomplete} /> + </main> + </EuiPage> + </UptimeAlertsContextProvider> + </UptimeStartupPluginsContextProvider> </UptimeThemeContextProvider> </UptimeSettingsContextProvider> </UptimeRefreshContextProvider> diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 725b53aeca02d..d68bbabe82b86 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -5,7 +5,7 @@ */ import { Request, Server } from 'hapi'; -import { PLUGIN } from '../../../legacy/plugins/uptime/common/constants'; +import { PLUGIN } from '../common/constants'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 98c6be5aa3c8e..f4d1c72770494 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -13,7 +13,7 @@ import { } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; -import { DynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../common/runtime_types'; export type APICaller = ( endpoint: string, @@ -31,7 +31,7 @@ export type UMSavedObjectsQueryFn<T = any, P = undefined> = ( ) => Promise<T> | T; export interface UptimeCoreSetup { - route: IRouter; + router: IRouter; } export interface UptimeCorePlugins { diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index 0176471aec1be..46f46720d4c04 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -21,10 +21,10 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte }; switch (method) { case 'GET': - this.server.route.get(routeDefinition, handler); + this.server.router.get(routeDefinition, handler); break; case 'POST': - this.server.route.post(routeDefinition, handler); + this.server.router.post(routeDefinition, handler); break; default: throw new Error(`Handler for method ${method} is not defined`); diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 4f4c6e3011ad1..24da3f3fa4d06 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,7 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; -import { 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 829e6f92d3702..f9df559a3977b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -9,14 +9,14 @@ import { isRight } from 'fp-ts/lib/Either'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; import { AlertExecutorOptions } from '../../../../alerting/server'; -import { ACTION_GROUP_DEFINITIONS } from '../../../../../legacy/plugins/uptime/common/constants'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; import { StatusCheckExecutorParamsType, StatusCheckAlertStateType, StatusCheckAlertState, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; import { savedObjectsAdapter } from '../saved_objects'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; diff --git a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts index fb44f5727aab3..26515fb4b4c63 100644 --- a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -5,7 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../common/constants'; export const parseRelativeDate = (dateStr: string, options = {}) => { // We need this this parsing because if user selects This week or this date diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index b49a6b22ff976..894e2316dc927 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 03e2bc7a44bd0..75bf5096bd997 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 5d3f9ce8d4ad9..45be1df3e8d3b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { 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 e47be617d7c99..82e624221c301 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -6,8 +6,8 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { 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 4de7d3ffd2a7d..e456670a5e68d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,7 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { 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 abd3655cc6402..fd890a30cf742 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,7 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; -import { 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 4f99fbe94d54c..b427e7cae1a7e 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 95d23ddcbf466..dbe71cf689214 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { OverviewFilters } from '../../../common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; export interface GetFilterBarParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index 6f7854d35b308..7688f04f1acd9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { StatesIndexStatus } from '../../../common/runtime_types'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a8e9ccb875a08..98ce449002f21 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 4ce7176b57b19..cf4ffa339ddfc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -5,10 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { - MonitorDetails, - MonitorError, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts index e9c745b0a8713..ea2a7e790652b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - LocationDurationLine, - MonitorDurationResult, -} from '../../../../../legacy/plugins/uptime/common/types'; +import { LocationDurationLine, MonitorDurationResult } from '../../../common/types'; +import { QUERY } from '../../../common/constants'; export interface GetMonitorChartsParams { /** @member monitorId ID value for the selected monitor */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index f49e404ffb084..c8d3ca043edc5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { UNNAMED_LOCATION } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - MonitorLocations, - MonitorLocation, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; +import { UNNAMED_LOCATION } from '../../../common/constants'; /** * Fetch data for the monitor page title. diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 4b40943a85705..b1791dd04861c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -4,15 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; -import { - SortOrder, - CursorDirection, - MonitorSummary, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; - +import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 5a8927764ea5c..299913c8dff08 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,12 +5,9 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { - HistogramResult, - HistogramQueryResult, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { HistogramResult, HistogramQueryResult } from '../../../common/runtime_types'; +import { QUERY } from '../../../common/constants'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 6eccfdb13cef7..a6a0e3c3d6542 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -10,7 +10,7 @@ import { HttpResponseBody, PingsResponse, Ping, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; const DEFAULT_PAGE_SIZE = 25; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 01f2ad88161cf..b57bc87d45418 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { Snapshot } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface GetSnapshotCountParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index 2a8f681ab3453..d4ad80c85ec3d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 84774cdeed856..e53fff429dd8d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -6,10 +6,7 @@ import { QueryContext } from '../query_context'; import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 47034c2130116..40775bde1c7f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -5,10 +5,7 @@ */ import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 4739c804d24e7..d21259fad77a6 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 84167840d5d9b..bef8106ad1896 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -7,12 +7,8 @@ import { flatten } from 'lodash'; import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../../../legacy/plugins/uptime/common/constants'; -import { - CursorDirection, - MonitorSummary, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { QUERY } from '../../../../common/constants'; +import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/runtime_types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 3449febfa5b05..e60c52660915a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index 31d9166eb1e73..2fb9562028258 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 43fc54fb25808..977c32ad1f984 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -5,7 +5,7 @@ */ import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts index 2ec52d400b597..35e9647196454 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CursorDirection, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 84154429b9188..69507d2950cd8 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 d849fbd8ce0a8..28b9eaad2cf6f 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 7cc591a6b2db1..13d1ae216f204 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -8,19 +8,20 @@ import { PluginInitializerContext, CoreStart, CoreSetup, + Plugin as PluginType, ISavedObjectsRepository, } from '../../../../src/core/server'; import { initServerWithKibana } from './kibana.index'; import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters'; import { umDynamicSettings } from './lib/saved_objects'; -export class Plugin { +export class Plugin implements PluginType { private savedObjectsClient?: ISavedObjectsRepository; constructor(_initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana({ route: core.http.createRouter() }, plugins); + initServerWithKibana({ router: core.http.createRouter() }, plugins); core.savedObjects.registerType(umDynamicSettings); KibanaTelemetryAdapter.registerUsageCollector( plugins.usageCollection, @@ -28,7 +29,9 @@ export class Plugin { ); } - public start(_core: CoreStart, _plugins: any) { - this.savedObjectsClient = _core.savedObjects.createInternalRepository(); + public start(core: CoreStart, _plugins: any) { + this.savedObjectsClient = core.savedObjects.createInternalRepository(); } + + public stop() {} } diff --git a/x-pack/plugins/uptime/server/rest_api/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs.ts index 31fb3f4ab96a7..f2e1700b23e7d 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 3f4e2fc345182..31833a25ee8ac 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -8,10 +8,7 @@ import { schema } from '@kbn/config-schema'; import { isRight } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../lib/lib'; -import { - DynamicSettings, - DynamicSettingsType, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings, DynamicSettingsType } from '../../common/runtime_types'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects'; diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 689a75c5903a6..26715f0ff37b6 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 8ed73d90b2389..9a4280efa98f9 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 5cb4e8a6241b7..60b3eafaa765e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { UMRestApiRouteFactory } from '../types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS, CONTEXT_DEFAULTS } from '../../../common/constants'; export const createMonitorListRoute: UMRestApiRouteFactory = libs => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 66ce9871506d4..a110209043a7e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 9cf1340fb9409..bb002f8a8c286 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 1cc010781457e..69e719efb0719 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index 9743ced13350a..34313211061b0 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index deac05f36c8dc..00cbaf0d16723 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { objectValuesToArrays } from '../../lib/helper'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; const arrayOrStringType = schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index dceef5ecb7848..41078f735920b 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 80a887a7f64a9..d97195a7fe2b1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -9,8 +9,8 @@ import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; -import { GetPingsParamsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../common/constants'; +import { GetPingsParamsType } from '../../../common/runtime_types'; export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index d870f49280117..7809e102a499f 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 4b2db71037071..d8387e79e9089 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { KibanaTelemetryAdapter } from '../../lib/adapters/telemetry'; import { UMRestApiRouteFactory } from '../types'; import { PageViewParams } from '../../lib/adapters/telemetry/types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ method: 'POST', diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index e05e7a4d7faf1..8720b9dc60b12 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -15,8 +15,8 @@ import { KibanaRequest, KibanaResponseFactory, IKibanaResponse, -} from 'src/core/server'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +} from 'kibana/server'; +import { DynamicSettings } from '../../common/runtime_types'; import { UMServerLibs } from '../lib/lib'; /** diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 974f3eb6db60f..efe1f85905970 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -14,7 +14,6 @@ const alwaysImportedTests = [ ]; const onlyNotInCoverageTests = [ require.resolve('../test/reporting/configs/chromium_api.js'), - require.resolve('../test/reporting/configs/chromium_functional.js'), require.resolve('../test/reporting/configs/generate_api.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index a7551ad7e2fad..1244657ed9988 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts index 46258e41d5d69..4151deab45213 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts index 338610e9243a4..bae6dada48fb7 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 457b7621e84bd..870ed3cf0cc0f 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 05139213b76b9..400aec7e11c8d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -21,7 +21,7 @@ interface CheckAADRequest extends Hapi.Request { // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['actions', 'alerting', 'encryptedSavedObjects'], + require: ['encryptedSavedObjects'], name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts deleted file mode 100644 index 019b15cc1862a..0000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Hapi from 'hapi'; -import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../plugins/actions/server/plugin'; -import { ActionType } from '../../../../../../plugins/actions/server'; - -import { initPlugin as initPagerduty } from './pagerduty_simulation'; -import { initPlugin as initServiceNow } from './servicenow_simulation'; -import { initPlugin as initSlack } from './slack_simulation'; -import { initPlugin as initWebhook } from './webhook_simulation'; - -const NAME = 'actions-FTS-external-service-simulators'; - -export enum ExternalServiceSimulator { - PAGERDUTY = 'pagerduty', - SERVICENOW = 'servicenow', - SLACK = 'slack', - WEBHOOK = 'webhook', -} - -export function getExternalServiceSimulatorPath(service: ExternalServiceSimulator): string { - return `/api/_${NAME}/${service}`; -} - -export function getAllExternalServiceSimulatorPaths(): string[] { - const allPaths = Object.values(ExternalServiceSimulator).map(service => - getExternalServiceSimulatorPath(service) - ); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); - return allPaths; -} - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - require: ['xpack_main', 'actions'], - name: NAME, - init: (server: Hapi.Server) => { - // this action is specifically NOT enabled in ../../config.ts - const notEnabledActionType: ActionType = { - id: 'test.not-enabled', - name: 'Test: Not Enabled', - minimumLicenseRequired: 'gold', - async executor() { - return { status: 'ok', actionId: '' }; - }, - }; - (server.newPlatform.setup.plugins.actions as ActionsPluginSetupContract).registerType( - notEnabledActionType - ); - server.plugins.xpack_main.registerFeature({ - id: 'actions', - name: 'Actions', - app: ['actions', 'kibana'], - privileges: { - all: { - app: ['actions', 'kibana'], - savedObject: { - all: ['action', 'action_task_params'], - read: [], - }, - ui: [], - api: ['actions-read', 'actions-all'], - }, - read: { - app: ['actions', 'kibana'], - savedObject: { - all: ['action_task_params'], - read: ['action'], - }, - ui: [], - api: ['actions-read'], - }, - }, - }); - - initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); - initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); - initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); - initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); - }, - }); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts new file mode 100644 index 0000000000000..45edd4c092da9 --- /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 43d533ad3ae14..1a47addf36ab3 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -11,8 +11,8 @@ 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({ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts index 29708f86b0a9b..ac32f05805e4a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts @@ -31,7 +31,7 @@ const taskByIdQuery = (id: string) => ({ export default function(kibana: any) { return new kibana.Plugin({ name: 'taskManagerHelpers', - require: ['elasticsearch', 'task_manager'], + require: ['elasticsearch'], config(Joi: any) { return Joi.object({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index eeb0818b5fbab..4c76ebfb93b0b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 054f8f6141817..399ae0f27f5b1 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 e00589b7e85b7..386254e49c19c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index fd996ea4507ba..9b66326fa6157 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; const defaultValues: Record<string, any> = { headers: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index 5122a74d53b72..112149a32649a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index e8d336e875b99..73fe435764b74 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -27,72 +27,65 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); expect(body.events.length).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - expect(body.pagination.total).to.eql(1); - // default limit - expect(body.pagination.limit).to.eql(100); + expect(body.pagination.nextEvent).to.eql(null); }); it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.events).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextEvent).to.eql(null); }); it('should return the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.pagination.nextEvent).to.eql(null); }); it('should error on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/events?events=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/events?events=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/events?events=-1`) .set(commonHeaders) .expect(400); }); it('should not find any events', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/5555/related`) + .get(`/api/endpoint/resolver/5555/events`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); it('should return no results for an invalid endpoint ID', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=foo`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); }); @@ -103,48 +96,46 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}&ancestors=5`) + .get( + `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=5` + ) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(2); - expect(body.ancestors.length).to.eql(1); - expect(body.pagination.next).to.eql(null); - // 5 is default parameter - expect(body.pagination.ancestors).to.eql(5); + expect(body.parent).not.to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should have a populated next parameter', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.next).to.eql('94041'); + expect(body.pagination.nextAncestor).to.eql('94041'); }); it('should handle an ancestors param request', async () => { let { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - const next = body.pagination.next; + const next = body.pagination.nextAncestor; ({ body } = await supertest - .get(`/api/endpoint/resolver/${next}?legacyEndpointID=${endpointID}&ancestors=1`) + .get(`/api/endpoint/resolver/${next}/ancestry?legacyEndpointID=${endpointID}&ancestors=1`) .set(commonHeaders) .expect(200)); expect(body.lifecycle.length).to.eql(1); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should handle an invalid id', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/alskdjflasj`) + .get(`/api/endpoint/resolver/alskdjflasj/ancestry`) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(0); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); }); @@ -158,51 +149,58 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - // default limit - expect(body.pagination.limit).to.eql(10); - expect(body.children.length).to.eql(1); expect(body.children[0].lifecycle.length).to.eql(2); expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(94042); }); + it('returns multiple levels of child process lifecycle events', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&generations=3`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextChild).to.be(null); + expect(body.children[0].pagination.nextChild).to.be(null); + + expect(body.children.length).to.eql(8); + expect(body.children[0].children[0].lifecycle.length).to.eql(2); + expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(93932); + }); + it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.children).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextChild).to.eql(null); }); it('returns the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.children.length).to.eql(1); + expect(body.pagination.nextChild).to.be(null); }); it('errors on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/children?children=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/children?children=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/children?children=-1`) .set(commonHeaders) .expect(400); }); @@ -212,8 +210,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/5555/children`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); @@ -222,10 +219,26 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); }); + + describe('tree endpoint', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + + it('returns ancestors, events, children, and current process lifecycle', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextAncestor).to.equal(null); + expect(body.pagination.nextEvent).to.equal(null); + expect(body.pagination.nextChild).to.equal(null); + expect(body.children.length).to.equal(0); + expect(body.events.length).to.equal(0); + expect(body.lifecycle.length).to.equal(2); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index 8bb3475da6cc9..28a317893f5b2 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./log_entries')); loadTestFile(require.resolve('./log_entry_highlights')); loadTestFile(require.resolve('./logs_without_millis')); + loadTestFile(require.resolve('./log_sources')); loadTestFile(require.resolve('./log_summary')); loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./sources')); diff --git a/x-pack/test/api_integration/apis/infra/log_sources.ts b/x-pack/test/api_integration/apis/infra/log_sources.ts new file mode 100644 index 0000000000000..73d59bcdcd9a4 --- /dev/null +++ b/x-pack/test/api_integration/apis/infra/log_sources.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { beforeEach } from 'mocha'; +import { + getLogSourceConfigurationSuccessResponsePayloadRT, + patchLogSourceConfigurationSuccessResponsePayloadRT, +} from '../../../../plugins/infra/common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../plugins/infra/common/runtime_types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const logSourceConfiguration = getService('infraLogSourceConfiguration'); + + describe('log sources api', () => { + before(() => esArchiver.load('infra/metrics_and_logs')); + after(() => esArchiver.unload('infra/metrics_and_logs')); + beforeEach(() => esArchiver.load('empty_kibana')); + afterEach(() => esArchiver.unload('empty_kibana')); + + describe('source configuration get method for non-existant source', () => { + it('returns the default source configuration', async () => { + const response = await logSourceConfiguration + .createGetLogSourceConfigurationAgent('default') + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(origin).to.be('fallback'); + expect(configuration.name).to.be('Default'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + }); + }); + + describe('source configuration patch method for non-existant source', () => { + it('creates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + description: 'DESCRIPTION', + logAlias: 'filebeat-**', + fields: { + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + logColumns: [ + { + messageColumn: { + id: 'MESSAGE_COLUMN', + }, + }, + ], + }) + .expect(200); + + // check direct response + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-**'); + expect(configuration.fields.timestamp).to.be('TIMESTAMP'); + expect(configuration.fields.tiebreaker).to.be('TIEBREAKER'); + expect(configuration.logColumns).to.have.length(1); + expect(configuration.logColumns[0]).to.have.key('messageColumn'); + + // check for persistence + const { + data: { configuration: persistedConfiguration }, + } = await logSourceConfiguration.getLogSourceConfiguration('default'); + + expect(configuration).to.eql(persistedConfiguration); + }); + + it('creates a source configuration with default values for unspecified properties', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', {}) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('Default'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns).to.have.length(3); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + + // check for persistence + const { + data: { configuration: persistedConfiguration, origin: persistedOrigin }, + } = await logSourceConfiguration.getLogSourceConfiguration('default'); + + expect(persistedOrigin).to.be('stored'); + expect(configuration).to.eql(persistedConfiguration); + }); + }); + + describe('source configuration patch method for existing source', () => { + beforeEach(async () => { + await logSourceConfiguration.updateLogSourceConfiguration('default', {}); + }); + + it('updates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + description: 'DESCRIPTION', + logAlias: 'filebeat-**', + fields: { + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + logColumns: [ + { + messageColumn: { + id: 'MESSAGE_COLUMN', + }, + }, + ], + }) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-**'); + expect(configuration.fields.timestamp).to.be('TIMESTAMP'); + expect(configuration.fields.tiebreaker).to.be('TIEBREAKER'); + expect(configuration.logColumns).to.have.length(1); + expect(configuration.logColumns[0]).to.have.key('messageColumn'); + }); + + it('partially updates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + }) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns).to.have.length(3); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 4f17f9db67483..19879f5761ab2 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); + }); + it('should work with a filterQuery in KQL format', async () => { + const searchBody = getElasticsearchMetricQuery( + getSearchParams('avg'), + undefined, + '"agent.hostname":"foo"' + ); + const result = await client.search({ + index, + body: searchBody, + }); + expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); describe('querying with a groupBy parameter', () => { @@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); }); 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 bbc766df34dcf..10857caab98e2 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 6a57db1687868..a5cb68d782126 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 23ddd3b63a2ef..c42fc95c1bc7f 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 ef7e48388ff66..fcdf268ff27b0 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -56,7 +56,7 @@ export default function({ getService }: FtrProviderContext) { expect(body.now).to.be.a('number'); expect(body.idleTimeoutExpiration).to.be.a('number'); expect(body.lifespanExpiration).to.be(null); - expect(body.provider).to.be('basic'); + expect(body.provider).to.eql({ type: 'basic', name: 'basic' }); }); it('should not extend the session', async () => { diff --git a/x-pack/test/api_integration/apis/siem/authentications.ts b/x-pack/test/api_integration/apis/siem/authentications.ts index cf9d8d8c9a515..b89a1448d5fe6 100644 --- a/x-pack/test/api_integration/apis/siem/authentications.ts +++ b/x-pack/test/api_integration/apis/siem/authentications.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { authenticationsQuery } from '../../../../legacy/plugins/siem/public/containers/authentications/index.gql_query'; -import { GetAuthenticationsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { authenticationsQuery } from '../../../../plugins/siem/public/containers/authentications/index.gql_query'; +import { GetAuthenticationsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/hosts.ts b/x-pack/test/api_integration/apis/siem/hosts.ts index 6e74375cedc12..0a2ee9c82bce2 100644 --- a/x-pack/test/api_integration/apis/siem/hosts.ts +++ b/x-pack/test/api_integration/apis/siem/hosts.ts @@ -12,10 +12,10 @@ import { GetHostFirstLastSeenQuery, GetHostsTableQuery, HostsFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; -import { HostOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query'; -import { HostFirstLastSeenGqlQuery } from './../../../../legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query'; -import { HostsTableQuery } from './../../../../legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query'; +} from '../../../../plugins/siem/public/graphql/types'; +import { HostOverviewQuery } from '../../../../plugins/siem/public/containers/hosts/overview/host_overview.gql_query'; +import { HostFirstLastSeenGqlQuery } from '../../../../plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query'; +import { HostsTableQuery } from '../../../../plugins/siem/public/containers/hosts/hosts_table.gql_query'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/ip_overview.ts b/x-pack/test/api_integration/apis/siem/ip_overview.ts index bec31a018cac8..2f1a792aff25b 100644 --- a/x-pack/test/api_integration/apis/siem/ip_overview.ts +++ b/x-pack/test/api_integration/apis/siem/ip_overview.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/ip_overview/index.gql_query'; -import { GetIpOverviewQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { ipOverviewQuery } from '../../../../plugins/siem/public/containers/ip_overview/index.gql_query'; +import { GetIpOverviewQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts index 02152c0b71ca9..30f9f6f04a242 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiHostDetailsQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query'; -import { GetKpiHostDetailsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiHostDetailsQuery } from '../../../../plugins/siem/public/containers/kpi_host_details/index.gql_query'; +import { GetKpiHostDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 9e43405b324fa..2303b9ecfb78f 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiHostsQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query'; -import { GetKpiHostsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiHostsQuery } from '../../../../plugins/siem/public/containers/kpi_hosts/index.gql_query'; +import { GetKpiHostsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_network.ts b/x-pack/test/api_integration/apis/siem/kpi_network.ts index 6b12b4e3c938a..22e133e48bbd2 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_network.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_network.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiNetworkQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_network/index.gql_query'; -import { GetKpiNetworkQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiNetworkQuery } from '../../../../plugins/siem/public/containers/kpi_network/index.gql_query'; +import { GetKpiNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/network_dns.ts b/x-pack/test/api_integration/apis/siem/network_dns.ts index 13e98f09d072b..1eba41e238c81 100644 --- a/x-pack/test/api_integration/apis/siem/network_dns.ts +++ b/x-pack/test/api_integration/apis/siem/network_dns.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; -import { networkDnsQuery } from '../../../../legacy/plugins/siem/public/containers/network_dns/index.gql_query'; +import { networkDnsQuery } from '../../../../plugins/siem/public/containers/network_dns/index.gql_query'; import { Direction, GetNetworkDnsQuery, NetworkDnsFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts index ee4344bb0f1ee..6ab7945e9000d 100644 --- a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { networkTopNFlowQuery } from '../../../../legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query'; +import { networkTopNFlowQuery } from '../../../../plugins/siem/public/containers/network_top_n_flow/index.gql_query'; import { Direction, FlowTargetSourceDest, GetNetworkTopNFlowQuery, NetworkTopTablesFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const EDGE_LENGTH = 10; diff --git a/x-pack/test/api_integration/apis/siem/overview_host.ts b/x-pack/test/api_integration/apis/siem/overview_host.ts index 7e5cbd7673af7..95dbb44e30c41 100644 --- a/x-pack/test/api_integration/apis/siem/overview_host.ts +++ b/x-pack/test/api_integration/apis/siem/overview_host.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { DEFAULT_INDEX_PATTERN } from '../../../../plugins/siem/common/constants'; -import { overviewHostQuery } from '../../../../legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query'; -import { GetOverviewHostQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { overviewHostQuery } from '../../../../plugins/siem/public/containers/overview/overview_host/index.gql_query'; +import { GetOverviewHostQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/overview_network.ts b/x-pack/test/api_integration/apis/siem/overview_network.ts index ce2f8d4a0b5e5..ef7d82d2ea8d9 100644 --- a/x-pack/test/api_integration/apis/siem/overview_network.ts +++ b/x-pack/test/api_integration/apis/siem/overview_network.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { overviewNetworkQuery } from '../../../../legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query'; -import { GetOverviewNetworkQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { overviewNetworkQuery } from '../../../../plugins/siem/public/containers/overview/overview_network/index.gql_query'; +import { GetOverviewNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts index 5aa7a10e07c2a..75670374b6f63 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import gql from 'graphql-tag'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelineNoteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query'; +import { persistTimelineNoteMutation } from '../../../../../plugins/siem/public/containers/timeline/notes/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts index f1e926b3ede42..39055e971d118 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelinePinnedEventMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query'; +import { persistTimelinePinnedEventMutation } from '../../../../../plugins/siem/public/containers/timeline/pinned_event/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts index 6fe11bc294795..2d9f576ef37e9 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts @@ -15,10 +15,10 @@ import ApolloClient from 'apollo-client'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query'; -import { persistTimelineFavoriteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query'; -import { persistTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/persist.gql_query'; -import { TimelineResult } from '../../../../../legacy/plugins/siem/public/graphql/types'; +import { deleteTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/delete/persist.gql_query'; +import { persistTimelineFavoriteMutation } from '../../../../../plugins/siem/public/containers/timeline/favorite/persist.gql_query'; +import { persistTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/persist.gql_query'; +import { TimelineResult } from '../../../../../plugins/siem/public/graphql/types'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/sources.ts b/x-pack/test/api_integration/apis/siem/sources.ts index e0db91449a8cc..2338d4ce45c8d 100644 --- a/x-pack/test/api_integration/apis/siem/sources.ts +++ b/x-pack/test/api_integration/apis/siem/sources.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { sourceQuery } from '../../../../legacy/plugins/siem/public/containers/source/index.gql_query'; -import { SourceQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { sourceQuery } from '../../../../plugins/siem/public/containers/source/index.gql_query'; +import { SourceQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/timeline.ts b/x-pack/test/api_integration/apis/siem/timeline.ts index a6ab4493448f3..de57b0c3f469f 100644 --- a/x-pack/test/api_integration/apis/siem/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/timeline.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { timelineQuery } from '../../../../legacy/plugins/siem/public/containers/timeline/index.gql_query'; -import { Direction, GetTimelineQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { timelineQuery } from '../../../../plugins/siem/public/containers/timeline/index.gql_query'; +import { Direction, GetTimelineQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const LTE = new Date('3000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/timeline_details.ts b/x-pack/test/api_integration/apis/siem/timeline_details.ts index 5d1e645bcf80b..f88d5355f22c1 100644 --- a/x-pack/test/api_integration/apis/siem/timeline_details.ts +++ b/x-pack/test/api_integration/apis/siem/timeline_details.ts @@ -7,11 +7,8 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { timelineDetailsQuery } from '../../../../legacy/plugins/siem/public/containers/timeline/details/index.gql_query'; -import { - DetailItem, - GetTimelineDetailsQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +import { timelineDetailsQuery } from '../../../../plugins/siem/public/containers/timeline/details/index.gql_query'; +import { DetailItem, GetTimelineDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; type DetailsData = Array< diff --git a/x-pack/test/api_integration/apis/siem/tls.ts b/x-pack/test/api_integration/apis/siem/tls.ts index 8467308d709af..e4e8b5db3d7e3 100644 --- a/x-pack/test/api_integration/apis/siem/tls.ts +++ b/x-pack/test/api_integration/apis/siem/tls.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { tlsQuery } from '../../../../legacy/plugins/siem/public/containers/tls/index.gql_query'; +import { tlsQuery } from '../../../../plugins/siem/public/containers/tls/index.gql_query'; import { Direction, TlsFields, FlowTarget, GetTlsQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts index b463c4db99653..c9674e740f76d 100644 --- a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { uncommonProcessesQuery } from '../../../../legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query'; -import { GetUncommonProcessesQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { uncommonProcessesQuery } from '../../../../plugins/siem/public/containers/uncommon_processes/index.gql_query'; +import { GetUncommonProcessesQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/users.ts b/x-pack/test/api_integration/apis/siem/users.ts index f1869d40fbf1d..c8ea1be7d3f11 100644 --- a/x-pack/test/api_integration/apis/siem/users.ts +++ b/x-pack/test/api_integration/apis/siem/users.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { usersQuery } from '../../../../legacy/plugins/siem/public/containers/users/index.gql_query'; +import { usersQuery } from '../../../../plugins/siem/public/containers/users/index.gql_query'; import { Direction, UsersFields, FlowTarget, GetUsersQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 6d125807e169d..6c566ec7cb23b 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; -import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../plugins/uptime/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts index 7510ea3f34d28..a3a15d8f8b014 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 5258426cf193c..f343cd1da8788 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; import { expectFixtureEql } from './helper/expect_fixture_eql'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { describe('docCount query', () => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index ea980721b831b..95caf50d1ca7a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -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 3c17370532f91..c3d5849e028ab 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { makeChecksWithStatus } from './helper/make_checks'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummary } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index f1e37bff405fd..c5a691312f525 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummaryResultType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummaryResultType } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; interface ExpectedMonitorStatesPage { response: any; diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts index a261763d5991f..3d754d89cf9be 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { PingsResponseType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { PingsResponseType } from '../../../../../plugins/uptime/common/runtime_types'; import { FtrProviderContext } from '../../../ftr_provider_context'; function decodePingsResponseData(response: any) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index 017ef02afe5ea..99e09aa5ce886 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 84b8476bd1dd1..6dcc9bb291b02 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -21,6 +21,7 @@ import { } from './infraops_graphql_client'; import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; +import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration'; import { MachineLearningProvider } from './ml'; import { IngestManagerProvider } from './ingest_manager'; @@ -35,6 +36,7 @@ export const services = { infraOpsGraphQLClient: InfraOpsGraphQLClientProvider, infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider, infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider, + infraLogSourceConfiguration: InfraLogSourceConfigurationProvider, siemGraphQLClient: SiemGraphQLClientProvider, siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, diff --git a/x-pack/test/api_integration/services/infra_log_source_configuration.ts b/x-pack/test/api_integration/services/infra_log_source_configuration.ts new file mode 100644 index 0000000000000..851720895c620 --- /dev/null +++ b/x-pack/test/api_integration/services/infra_log_source_configuration.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceConfigurationPath, + getLogSourceConfigurationSuccessResponsePayloadRT, + PatchLogSourceConfigurationRequestBody, + patchLogSourceConfigurationRequestBodyRT, + patchLogSourceConfigurationResponsePayloadRT, +} from '../../../plugins/infra/common/http_api/log_sources'; +import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function InfraLogSourceConfigurationProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + const createGetLogSourceConfigurationAgent = (sourceId: string) => + supertest + .get(getLogSourceConfigurationPath(sourceId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(); + + const getLogSourceConfiguration = async (sourceId: string) => { + log.debug(`Fetching Logs UI source configuration "${sourceId}"`); + + const response = await createGetLogSourceConfigurationAgent(sourceId); + + return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + }; + + const createUpdateLogSourceConfigurationAgent = ( + sourceId: string, + sourceProperties: PatchLogSourceConfigurationRequestBody['data'] + ) => + supertest + .patch(getLogSourceConfigurationPath(sourceId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(patchLogSourceConfigurationRequestBodyRT.encode({ data: sourceProperties })); + + const updateLogSourceConfiguration = async ( + sourceId: string, + sourceProperties: PatchLogSourceConfigurationRequestBody['data'] + ) => { + log.debug( + `Updating Logs UI source configuration "${sourceId}" with properties ${JSON.stringify( + sourceProperties + )}` + ); + + const response = await createUpdateLogSourceConfigurationAgent(sourceId, sourceProperties); + + return decodeOrThrow(patchLogSourceConfigurationResponsePayloadRT)(response.body); + }; + + return { + createGetLogSourceConfigurationAgent, + createUpdateLogSourceConfigurationAgent, + getLogSourceConfiguration, + updateLogSourceConfiguration, + }; +} diff --git a/x-pack/test/api_integration/services/siem_graphql_client.ts b/x-pack/test/api_integration/services/siem_graphql_client.ts index 7d6aa5e8a9dd5..ba60e66682955 100644 --- a/x-pack/test/api_integration/services/siem_graphql_client.ts +++ b/x-pack/test/api_integration/services/siem_graphql_client.ts @@ -11,7 +11,7 @@ import { ApolloClient } from 'apollo-client'; import { HttpLink } from 'apollo-link-http'; import { FtrProviderContext } from '../ftr_provider_context'; -import introspectionQueryResultData from '../../../legacy/plugins/siem/public/graphql/introspection.json'; +import introspectionQueryResultData from '../../../plugins/siem/public/graphql/introspection.json'; interface SiemGraphQLClientFactoryOptions { username?: string; diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index e89352118990a..1e6600c7cd2c0 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -78,7 +78,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 794bd2f1f0db3..23825836caad3 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/dashboard/reporting/README.md b/x-pack/test/functional/apps/dashboard/reporting/README.md new file mode 100644 index 0000000000000..3a2b8f5cc783f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/README.md @@ -0,0 +1,22 @@ +## The Dashboard Reporting Tests + +### Baseline snapshots + +The reporting tests create a few PNG reports and do a snapshot comparison against stored baselines. The baseline images are stored in `./reports/baseline`. + +### Updating the baselines + +Every now and then visual changes will be made that will require the snapshots to be updated. This is how you go about updating it. + +1. **Load the ES Archive containing the dashboard and data.** + This will load the test data into an Elasticsearch instance running via the functional test server: + ``` + node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/config.js + node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/config.js + ``` +2. **Generate the reports of the E-commerce dashboard in the Kibana UI.** + Navigate to `http://localhost:5620`, find the archived dashboard, and generate all the types of reports for which there are stored baseline images. +3. **Download the reports, and save them into the `reports/baseline` folder.** + Change the names of the PNG/PDF files to overwrite the stored baselines. + +The next time functional tests run, the generated reports will be compared to the latest image that you have saved :bowtie: \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.ts b/x-pack/test/functional/apps/dashboard/reporting/index.ts new file mode 100644 index 0000000000000..796e15b4e270f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { checkIfPngsMatch } from './lib/compare_pngs'; + +const writeFileAsync = promisify(fs.writeFile); +const mkdirAsync = promisify(fs.mkdir); + +const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const config = getService('config'); + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + + describe('Reporting', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('Print PDF button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Print Layout', () => { + it('downloads a PDF file', async function() { + // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs + // function is taking about 15 seconds per comparison in jenkins. + this.timeout(300000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.checkUsePrintLayout(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/pdf'); + }); + }); + + describe('Print PNG button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PNG Dash'); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Preserve Layout', () => { + it('matches baseline report', async function() { + const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { + const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); + await mkdirAsync(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); + await writeFileAsync(sessionReportPath, rawPdf); + return sessionReportPath; + }; + const getBaselineReportPath = (fileName: string, reportExt: string) => { + const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + }; + + this.timeout(300000); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPngReportingPanel(); + await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); + await PageObjects.reporting.clickGenerateReportButton(); + await PageObjects.reporting.removeForceSharedItemsContainerSize(); + + const url = await PageObjects.reporting.getReportURL(60000); + const reportData = await PageObjects.reporting.getRawPdfReportData(url); + const reportFileName = 'dashboard_preserve_layout'; + const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); + const percentSimilar = await checkIfPngsMatch( + sessionReportPath, + getBaselineReportPath(reportFileName, 'png'), + config.get('screenshots.directory'), + log + ); + + expect(percentSimilar).to.be.lessThan(0.1); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts new file mode 100644 index 0000000000000..b2eb645c8372c --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { promisify } from 'bluebird'; +import fs from 'fs'; +import path from 'path'; +import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; + +const mkdirAsync = promisify<void, fs.PathLike, { recursive: boolean }>(fs.mkdir); + +export async function checkIfPngsMatch( + actualpngPath: string, + baselinepngPath: string, + screenshotsDirectory: string, + log: any +) { + log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); + // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be + // stored. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await mkdirAsync(sessionDirectoryPath, { recursive: true }); + await mkdirAsync(failureDirectoryPath, { recursive: true }); + + const actualpngFileName = path.basename(actualpngPath, '.png'); + const baselinepngFileName = path.basename(baselinepngPath, '.png'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinepngFileName}_baseline.png` + ); + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); + + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFileSync: ${baselineCopyPath}`); + fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); + } catch (error) { + log.error(`No baseline png found at ${baselinepngPath}`); + return 0; + } + log.debug(`writeFileSync: ${actualCopyPath}`); + fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); + + let diffTotal = 0; + + const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); + diffTotal += await comparePngs( + actualCopyPath, + baselineCopyPath, + diffPngPath, + sessionDirectoryPath, + log + ); + + return diffTotal; +} diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/reporting/reports/baseline/dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 2e5bdd736337d..7688315a00516 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts new file mode 100644 index 0000000000000..7a33e7f5135d4 --- /dev/null +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['reporting', 'common', 'discover']); + const filterBar = getService('filterBar'); + + describe('Discover', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + }); + + describe('Generate CSV button', () => { + beforeEach(() => PageObjects.common.navigateToApp('discover')); + + it('is not available if new', async () => { + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('becomes available/not available when a saved search is created, changed and saved again', async () => { + // create new search, csv export is not available + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + // save search, csv export is available + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton 2'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + // add filter, csv export is not available + await filterBar.addFilter('currency', 'is', 'EUR'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + // save search again, csv export is available + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton 2'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('generates a report with data', async () => { + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.reporting.setTimepickerInDataRange(); + await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + }); + + it('generates a report with no data', async () => { + await PageObjects.reporting.setTimepickerInNoDataRange(); + await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index 4e5ebab90880e..89ed51f65b930 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { URL } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; const ONE_HOUR = 60 * 60 * 1000; @@ -28,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', @@ -41,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/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 64b6300e0df63..7a813a5cdfb52 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 f30367ba3dd0b..1c23b8cde8606 100644 --- a/x-pack/test/functional/apps/visualize/index.ts +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -15,5 +15,6 @@ export default function visualize({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./hybrid_visualization')); loadTestFile(require.resolve('./precalculated_histogram')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts new file mode 100644 index 0000000000000..5ef954e334d81 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const PageObjects = getPageObjects([ + 'reporting', + 'common', + 'dashboard', + 'visualize', + 'visEditor', + ]); + + describe('Visualize', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('Print PDF button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToUrl('visualize', 'new'); + await PageObjects.visualize.clickAreaChart(); + await PageObjects.visualize.clickNewSearch('ecommerce'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.reporting.setTimepickerInDataRange(); + await PageObjects.visEditor.clickBucket('X-axis'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); + await PageObjects.visualize.saveVisualization('my viz'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('downloaded PDF has OK status', async function() { + // Generating and then comparing reports can take longer than the default 60s timeout + this.timeout(180000); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/pdf'); + }); + }); + }); +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 143a0b956985c..833cc452a5d31 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page'; import { GrokDebuggerPageProvider } from './grok_debugger_page'; // @ts-ignore not ts yet import { WatcherPageProvider } from './watcher_page'; -// @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 1bf637c50b0ba..57b2847cc2e50 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -12,6 +12,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); + const comboBox = getService('comboBox'); const PageObjects = getPageObjects([ 'header', 'common', @@ -107,20 +108,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.operation - the desired operation ID for the dimension * @param opts.field - the desired field for the dimension */ - async configureDimension(opts: { dimension: string; operation?: string; field?: string }) { + async configureDimension(opts: { dimension: string; operation: string; field: string }) { await find.clickByCssSelector(opts.dimension); - if (opts.operation) { - await find.clickByCssSelector( - `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], - [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` - ); - } + await find.clickByCssSelector( + `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], + [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` + ); - if (opts.field) { - await testSubjects.click('indexPattern-dimension-field'); - await testSubjects.click(`lns-fieldOption-${opts.field}`); - } + const target = await testSubjects.find('indexPattern-dimension-field'); + await comboBox.openOptionsList(target); + await comboBox.setElement(target, opts.field); }, /** diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js deleted file mode 100644 index 35a76c74d4811..0000000000000 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ /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 { parse } from 'url'; -import http from 'http'; - -/* - * NOTE: Reporting is a service, not an app. The page objects that are - * important for generating reports belong to the apps that integrate with the - * Reporting service. Eventually, this file should be dissolved across the - * apps that need it for testing their integration. - * Issue: https://github.com/elastic/kibana/issues/52927 - */ -export function ReportingPageProvider({ getService, getPageObjects }) { - const retry = getService('retry'); - const log = getService('log'); - const config = getService('config'); - const testSubjects = getService('testSubjects'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); - - class ReportingPage { - async forceSharedItemsContainerSize({ width }) { - await browser.execute(` - var el = document.querySelector('[data-shared-items-container]'); - el.style.flex="none"; - el.style.width="${width}px"; - `); - } - - async getReportURL(timeout) { - log.debug('getReportURL'); - - const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); - - log.debug(`getReportURL got url: ${url}`); - - return url; - } - - async removeForceSharedItemsContainerSize() { - await browser.execute(` - var el = document.querySelector('[data-shared-items-container]'); - el.style.flex = null; - el.style.width = null; - `); - } - - getResponse(url) { - log.debug(`getResponse for ${url}`); - const auth = config.get('servers.elasticsearch.auth'); - const headers = { - Authorization: `Basic ${Buffer.from(auth).toString('base64')}`, - }; - const parsedUrl = parse(url); - return new Promise((resolve, reject) => { - http - .get( - { - hostname: parsedUrl.hostname, - path: parsedUrl.path, - port: parsedUrl.port, - responseType: 'arraybuffer', - headers, - }, - res => { - resolve(res); - } - ) - .on('error', e => { - reject(e); - }); - }); - } - - async getRawPdfReportData(url) { - const data = []; // List of Buffer objects - log.debug(`getRawPdfReportData for ${url}`); - - return new Promise(async (resolve, reject) => { - const response = await this.getResponse(url).catch(reject); - - response.on('data', chunk => data.push(chunk)); - response.on('end', () => resolve(Buffer.concat(data))); - }); - } - - async openCsvReportingPanel() { - log.debug('openCsvReportingPanel'); - await PageObjects.share.openShareMenuItem('CSV Reports'); - } - - async openPdfReportingPanel() { - log.debug('openPdfReportingPanel'); - await PageObjects.share.openShareMenuItem('PDF Reports'); - } - - async openPngReportingPanel() { - log.debug('openPngReportingPanel'); - await PageObjects.share.openShareMenuItem('PNG Reports'); - } - - async clearToastNotifications() { - const toasts = await testSubjects.findAll('toastCloseButton'); - await Promise.all(toasts.map(async t => await t.click())); - } - - async getQueueReportError() { - return await testSubjects.exists('queueReportError'); - } - - async getGenerateReportButton() { - return await retry.try(async () => await testSubjects.find('generateReportButton')); - } - - async isGenerateReportButtonDisabled() { - const generateReportButton = await this.getGenerateReportButton(); - return await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - return isDisabled; - }); - } - - async canReportBeCreated() { - await this.clickGenerateReportButton(); - const success = await this.checkForReportingToasts(); - return success; - } - - async checkUsePrintLayout() { - // The print layout checkbox slides in as part of an animation, and tests can - // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows - // the animation to complete before we attempt a click. - const menuAnimationDelay = 500; - await retry.tryForTime(menuAnimationDelay, () => testSubjects.click('usePrintLayout')); - } - - async clickGenerateReportButton() { - await testSubjects.click('generateReportButton'); - } - - async checkForReportingToasts() { - log.debug('Reporting:checkForReportingToasts'); - const isToastPresent = await testSubjects.exists('completeReportSuccess', { - allowHidden: true, - timeout: 90000, - }); - // Close toast so it doesn't obscure the UI. - if (isToastPresent) { - await testSubjects.click('completeReportSuccess > toastCloseButton'); - } - - return isToastPresent; - } - - async setTimepickerInDataRange() { - log.debug('Reporting:setTimepickerInDataRange'); - const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; - const toTime = 'Sep 19, 2015 @ 18:01:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - } - - async setTimepickerInNoDataRange() { - log.debug('Reporting:setTimepickerInNoDataRange'); - const fromTime = 'Sep 19, 1999 @ 06:31:44.000'; - const toTime = 'Sep 23, 1999 @ 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - } - } - return new ReportingPage(); -} diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts new file mode 100644 index 0000000000000..2c20519a8d214 --- /dev/null +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import http, { IncomingMessage } from 'http'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; +import { parse } from 'url'; + +export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript + + class ReportingPage { + async forceSharedItemsContainerSize({ width }: { width: number }) { + await browser.execute(` + var el = document.querySelector('[data-shared-items-container]'); + el.style.flex="none"; + el.style.width="${width}px"; + `); + } + + async getReportURL(timeout: number) { + log.debug('getReportURL'); + + const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); + + log.debug(`getReportURL got url: ${url}`); + + return url; + } + + async removeForceSharedItemsContainerSize() { + await browser.execute(` + var el = document.querySelector('[data-shared-items-container]'); + el.style.flex = null; + el.style.width = null; + `); + } + + getResponse(url: string): Promise<IncomingMessage> { + log.debug(`getResponse for ${url}`); + const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this + const headers = { + Authorization: `Basic ${Buffer.from(auth).toString('base64')}`, + }; + const parsedUrl = parse(url); + return new Promise((resolve, reject) => { + http + .get( + { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + port: parsedUrl.port, + headers, + }, + (res: IncomingMessage) => { + resolve(res); + } + ) + .on('error', (e: Error) => { + log.error(e); + reject(e); + }); + }); + } + + async getRawPdfReportData(url: string): Promise<Buffer> { + const data: Buffer[] = []; // List of Buffer objects + log.debug(`getRawPdfReportData for ${url}`); + + return new Promise(async (resolve, reject) => { + const response = await this.getResponse(url).catch(reject); + + if (response) { + response.on('data', (chunk: Buffer) => data.push(chunk)); + response.on('end', () => resolve(Buffer.concat(data))); + } + }); + } + + async openCsvReportingPanel() { + log.debug('openCsvReportingPanel'); + await PageObjects.share.openShareMenuItem('CSV Reports'); + } + + async openPdfReportingPanel() { + log.debug('openPdfReportingPanel'); + await PageObjects.share.openShareMenuItem('PDF Reports'); + } + + async openPngReportingPanel() { + log.debug('openPngReportingPanel'); + await PageObjects.share.openShareMenuItem('PNG Reports'); + } + + async clearToastNotifications() { + const toasts = await testSubjects.findAll('toastCloseButton'); + await Promise.all(toasts.map(async t => await t.click())); + } + + async getQueueReportError() { + return await testSubjects.exists('queueReportError'); + } + + async getGenerateReportButton() { + return await retry.try(async () => await testSubjects.find('generateReportButton')); + } + + async isGenerateReportButtonDisabled() { + const generateReportButton = await this.getGenerateReportButton(); + return await retry.try(async () => { + const isDisabled = await generateReportButton.getAttribute('disabled'); + return isDisabled; + }); + } + + async canReportBeCreated() { + await this.clickGenerateReportButton(); + const success = await this.checkForReportingToasts(); + return success; + } + + async checkUsePrintLayout() { + // The print layout checkbox slides in as part of an animation, and tests can + // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows + // the animation to complete before we attempt a click. + const menuAnimationDelay = 500; + await retry.tryForTime(menuAnimationDelay, () => testSubjects.click('usePrintLayout')); + } + + async clickGenerateReportButton() { + await testSubjects.click('generateReportButton'); + } + + async checkForReportingToasts() { + log.debug('Reporting:checkForReportingToasts'); + const isToastPresent = await testSubjects.exists('completeReportSuccess', { + allowHidden: true, + timeout: 90000, + }); + // Close toast so it doesn't obscure the UI. + if (isToastPresent) { + await testSubjects.click('completeReportSuccess > toastCloseButton'); + } + + return isToastPresent; + } + + async setTimepickerInDataRange() { + log.debug('Reporting:setTimepickerInDataRange'); + const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; + const toTime = 'Sep 19, 2015 @ 18:01:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + async setTimepickerInNoDataRange() { + log.debug('Reporting:setTimepickerInNoDataRange'); + const fromTime = 'Sep 19, 1999 @ 06:31:44.000'; + const toTime = 'Sep 23, 1999 @ 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + } + return new ReportingPage(); +} diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 9719152b62d35..96f5e45ce2ca4 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/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 2c29954528bd5..d0ce18bbc1c54 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 a620b1d953376..71b22a336f6b9 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 5b506c20e029c..2a0d28f246765 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -22,6 +22,44 @@ export class Alerts { }); } + public async createAlertWithActions( + name: string, + alertTypeId: string, + params?: Record<string, any>, + actions?: Array<{ + id: string; + group: string; + params: Record<string, any>; + }>, + tags?: string[], + consumer?: string, + schedule?: Record<string, any>, + throttle?: string + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags, + alertTypeId, + consumer: consumer ?? 'bar', + schedule: schedule ?? { interval: '1m' }, + throttle: throttle ?? '1m', + actions: actions ?? [], + params: params ?? {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + public async createNoOp(name: string) { this.log.debug(`creating alert ${name}`); diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index c581e0c246e13..adb31f3562a6f 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -22,6 +22,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./test_suites/task_manager'), require.resolve('./test_suites/event_log'), + require.resolve('./test_suites/licensed_feature_usage'), ], services, servers: integrationConfig.get('servers'), diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json new file mode 100644 index 0000000000000..b11b7ada24a57 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "feature_usage_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "feature_usage_test"], + "requiredPlugins": ["licensing"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.ts new file mode 100644 index 0000000000000..e07915ab5f46b --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'kibana/server'; +import { + FeatureUsageTestPlugin, + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart +> = () => new FeatureUsageTestPlugin(); diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts new file mode 100644 index 0000000000000..b36d6dca077f7 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; +import { + LicensingPluginSetup, + LicensingPluginStart, +} from '../../../../../plugins/licensing/server'; +import { registerRoutes } from './routes'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FeatureUsageTestPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FeatureUsageTestPluginStart {} + +export interface FeatureUsageTestSetupDependencies { + licensing: LicensingPluginSetup; +} +export interface FeatureUsageTestStartDependencies { + licensing: LicensingPluginStart; +} + +export class FeatureUsageTestPlugin + implements + Plugin< + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart, + FeatureUsageTestSetupDependencies, + FeatureUsageTestStartDependencies + > { + public setup( + { + http, + getStartServices, + }: CoreSetup<FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart>, + { licensing }: FeatureUsageTestSetupDependencies + ) { + licensing.featureUsage.register('test_feature_a'); + licensing.featureUsage.register('test_feature_b'); + licensing.featureUsage.register('test_feature_c'); + + registerRoutes(http.createRouter(), getStartServices); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts new file mode 100644 index 0000000000000..494bcdbf5f61e --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart } from '../plugin'; + +export function registerFeatureHitRoute( + router: IRouter, + getStartServices: StartServicesAccessor< + FeatureUsageTestStartDependencies, + FeatureUsageTestPluginStart + > +) { + router.get( + { + path: '/api/feature_usage_test/hit', + validate: { + query: schema.object({ + featureName: schema.string(), + usedAt: schema.maybe(schema.number()), + }), + }, + }, + async (context, request, response) => { + const [, { licensing }] = await getStartServices(); + try { + const { featureName, usedAt } = request.query; + licensing.featureUsage.notifyUsage(featureName, usedAt); + return response.ok(); + } catch (e) { + return response.badRequest(); + } + } + ); +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts new file mode 100644 index 0000000000000..a8225838fd9bf --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart } from '../plugin'; + +import { registerFeatureHitRoute } from './hit'; + +export function registerRoutes( + router: IRouter, + getStartServices: StartServicesAccessor< + FeatureUsageTestStartDependencies, + FeatureUsageTestPluginStart + > +) { + registerFeatureHitRoute(router, getStartServices); +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json new file mode 100644 index 0000000000000..416ef7fa34591 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "sample_task_plugin", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["taskManager"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json new file mode 100644 index 0000000000000..c8d47decd94c1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "sample_task_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/sample_task_plugin", + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + }, + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts new file mode 100644 index 0000000000000..77233f463734a --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SampleTaskManagerFixturePlugin } from './plugin'; + +export const plugin = () => new SampleTaskManagerFixturePlugin(); diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts new file mode 100644 index 0000000000000..1fee2decbcba9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IKibanaResponse, + IRouter, + CoreSetup, +} from 'kibana/server'; +import { EventEmitter } from 'events'; +import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; + +const scope = 'testing'; +const taskManagerQuery = { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'task.scope': scope, + }, + }, + ], + }, + }, + }, +}; + +export function initRoutes( + router: IRouter, + core: CoreSetup, + taskManagerStart: Promise<TaskManagerStartContract>, + taskTestingEvents: EventEmitter +) { + async function ensureIndexIsRefreshed() { + return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + index: '.kibana_task_manager', + }); + } + + router.post( + { + path: `/api/sample_tasks/schedule`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + schedule: schema.maybe( + schema.object({ + interval: schema.string(), + }) + ), + interval: schema.maybe(schema.string()), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + state: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const taskManager = await taskManagerStart; + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await taskManager.schedule(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/run_now`, + validate: { + body: schema.object({ + task: schema.object({ + id: schema.string({}), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + const { + task: { id }, + } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.runNow(id) }); + } catch (err) { + return res.ok({ body: { id, error: `${err}` } }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/ensure_scheduled`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + params: schema.object({}), + state: schema.maybe(schema.object({})), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskManager = await taskManagerStart; + const taskResult = await taskManager.ensureScheduled(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/event`, + validate: { + body: schema.object({ + event: schema.string(), + data: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const { event, data } = req.body; + taskTestingEvents.emit(event, data); + return res.ok({ body: event }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const taskManager = await taskManagerStart; + return res.ok({ + body: await taskManager.fetch({ + query: taskManagerQuery, + }), + }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks/task/{taskId}`, + validate: { + params: schema.object({ + taskId: schema.string(), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + await ensureIndexIsRefreshed(); + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.get(req.params.taskId) }); + } catch (err) { + return res.ok({ body: err }); + } + return res.ok({ body: {} }); + } + ); + + router.delete( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + let tasksFound = 0; + const taskManager = await taskManagerStart; + do { + const { docs: tasks } = await taskManager.fetch({ + query: taskManagerQuery, + }); + tasksFound = tasks.length; + await Promise.all(tasks.map(task => taskManager.remove(task.id))); + } while (tasksFound > 0); + return res.ok({ body: 'OK' }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts new file mode 100644 index 0000000000000..508d58b8f0ca9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { EventEmitter } from 'events'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { initRoutes } from './init_routes'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, + ConcreteTaskInstance, +} from '../../../../../plugins/task_manager/server'; +import { DEFAULT_MAX_WORKERS } from '../../../../../plugins/task_manager/server/config'; + +// this plugin's dependendencies +export interface SampleTaskManagerFixtureSetupDeps { + taskManager: TaskManagerSetupContract; +} +export interface SampleTaskManagerFixtureStartDeps { + taskManager: TaskManagerStartContract; +} + +export class SampleTaskManagerFixturePlugin + implements + Plugin<void, void, SampleTaskManagerFixtureSetupDeps, SampleTaskManagerFixtureStartDeps> { + taskManagerStart$: Subject<TaskManagerStartContract> = new Subject<TaskManagerStartContract>(); + taskManagerStart: Promise<TaskManagerStartContract> = this.taskManagerStart$ + .pipe(first()) + .toPromise(); + + public setup(core: CoreSetup, { taskManager }: SampleTaskManagerFixtureSetupDeps) { + const taskTestingEvents = new EventEmitter(); + taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); + + const defaultSampleTaskConfig = { + timeout: '1m', + // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) + // taskInstance.params has the following optional fields: + // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds + // failWith: string - If specified, the task will throw an error with the specified message + // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value + // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params + // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => ({ + async run() { + const { params, state, id } = taskInstance; + const prevState = state || { count: 0 }; + + const count = (prevState.count || 0) + 1; + + const runParams = { + ...params, + // if this task requires custom params provided async - wait for them + ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), + }; + + if (runParams.failWith) { + if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { + throw new Error(runParams.failWith); + } + } + + await core.elasticsearch.adminClient.callAsInternalUser('index', { + index: '.kibana_task_manager_test_result', + body: { + type: 'task', + taskId: taskInstance.id, + params: JSON.stringify(runParams), + state: JSON.stringify(state), + ranAt: new Date(), + }, + refresh: true, + }); + + // Stall task run until a certain event is triggered + if (runParams.waitForEvent) { + await once(taskTestingEvents, runParams.waitForEvent); + } + + return { + state: { count }, + runAt: millisecondsFromNow(runParams.nextRunMilliseconds), + }; + }, + }), + }; + + taskManager.registerTaskDefinitions({ + sampleTask: { + ...defaultSampleTaskConfig, + type: 'sampleTask', + title: 'Sample Task', + description: 'A sample task for testing the task_manager.', + }, + singleAttemptSampleTask: { + ...defaultSampleTaskConfig, + type: 'singleAttemptSampleTask', + title: 'Failing Sample Task', + description: + 'A sample task for testing the task_manager that fails on the first attempt to run.', + // fail after the first failed run + maxAttempts: 1, + }, + }); + + taskManager.addMiddleware({ + async beforeSave({ taskInstance, ...opts }) { + const modifiedInstance = { + ...taskInstance, + params: { + originalParams: taskInstance.params, + superFly: 'My middleware param!', + }, + }; + + return { + ...opts, + taskInstance: modifiedInstance, + }; + }, + + async beforeRun({ taskInstance, ...opts }) { + return { + ...opts, + taskInstance: { + ...taskInstance, + params: taskInstance.params.originalParams, + }, + }; + }, + + async beforeMarkRunning(context) { + return context; + }, + }); + initRoutes(core.http.createRouter(), core, this.taskManagerStart, taskTestingEvents); + } + + public start(core: CoreStart, { taskManager }: SampleTaskManagerFixtureStartDeps) { + this.taskManagerStart$.next(taskManager); + this.taskManagerStart$.complete(); + } + public stop() {} +} + +function millisecondsFromNow(ms: number) { + if (!ms) { + return; + } + + const dt = new Date(); + dt.setTime(dt.getTime() + ms); + return dt; +} + +const once = function(emitter: EventEmitter, event: string): Promise<Record<string, unknown>> { + return new Promise(resolve => { + emitter.once(event, data => resolve(data || {})); + }); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js deleted file mode 100644 index e5b645367b8b7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -const { DEFAULT_MAX_WORKERS } = require('../../../../plugins/task_manager/server/config.ts'); -const { EventEmitter } = require('events'); - -import { initRoutes } from './init_routes'; - -const once = function(emitter, event) { - return new Promise(resolve => { - emitter.once(event, data => resolve(data || {})); - }); -}; - -export default function TaskTestingAPI(kibana) { - const taskTestingEvents = new EventEmitter(); - taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); - - return new kibana.Plugin({ - name: 'sampleTask', - require: ['elasticsearch', 'task_manager'], - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - const taskManager = { - ...server.newPlatform.setup.plugins.taskManager, - ...server.newPlatform.start.plugins.taskManager, - }; - const legacyTaskManager = server.plugins.task_manager; - - const defaultSampleTaskConfig = { - timeout: '1m', - // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) - // taskInstance.params has the following optional fields: - // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds - // failWith: string - If specified, the task will throw an error with the specified message - // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value - // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params - // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing - createTaskRunner: ({ taskInstance }) => ({ - async run() { - const { params, state, id } = taskInstance; - const prevState = state || { count: 0 }; - - const count = (prevState.count || 0) + 1; - - const runParams = { - ...params, - // if this task requires custom params provided async - wait for them - ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), - }; - - if (runParams.failWith) { - if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { - throw new Error(runParams.failWith); - } - } - - const callCluster = server.plugins.elasticsearch.getCluster('admin') - .callWithInternalUser; - await callCluster('index', { - index: '.kibana_task_manager_test_result', - body: { - type: 'task', - taskId: taskInstance.id, - params: JSON.stringify(runParams), - state: JSON.stringify(state), - ranAt: new Date(), - }, - refresh: true, - }); - - // Stall task run until a certain event is triggered - if (runParams.waitForEvent) { - await once(taskTestingEvents, runParams.waitForEvent); - } - - return { - state: { count }, - runAt: millisecondsFromNow(runParams.nextRunMilliseconds), - }; - }, - }), - }; - - taskManager.registerTaskDefinitions({ - sampleTask: { - ...defaultSampleTaskConfig, - title: 'Sample Task', - description: 'A sample task for testing the task_manager.', - }, - singleAttemptSampleTask: { - ...defaultSampleTaskConfig, - title: 'Failing Sample Task', - description: - 'A sample task for testing the task_manager that fails on the first attempt to run.', - // fail after the first failed run - maxAttempts: 1, - }, - }); - - taskManager.addMiddleware({ - async beforeSave({ taskInstance, ...opts }) { - const modifiedInstance = { - ...taskInstance, - params: { - originalParams: taskInstance.params, - superFly: 'My middleware param!', - }, - }; - - return { - ...opts, - taskInstance: modifiedInstance, - }; - }, - - async beforeRun({ taskInstance, ...opts }) { - return { - ...opts, - taskInstance: { - ...taskInstance, - params: taskInstance.params.originalParams, - }, - }; - }, - }); - - initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents); - }, - }); -} - -function millisecondsFromNow(ms) { - if (!ms) { - return; - } - - const dt = new Date(); - dt.setTime(dt.getTime() + ms); - return dt; -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js deleted file mode 100644 index 785fbed341423..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -const scope = 'testing'; -const taskManagerQuery = { - bool: { - filter: { - bool: { - must: [ - { - term: { - 'task.scope': scope, - }, - }, - ], - }, - }, - }, -}; - -export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents) { - const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; - - async function ensureIndexIsRefreshed() { - return await callCluster('indices.refresh', { - index: '.kibana_task_manager', - }); - } - - server.route({ - path: '/api/sample_tasks/schedule', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - /* - Schedule using legacy Api - */ - server.route({ - path: '/api/sample_tasks/schedule_legacy', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await legacyTaskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/run_now', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - const { - task: { id }, - } = request.payload; - try { - return await taskManager.runNow(id); - } catch (err) { - return { id, error: `${err}` }; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/ensure_scheduled', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.ensureScheduled(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/event', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - event: Joi.string().required(), - data: Joi.object() - .optional() - .default({}), - }), - }, - }, - async handler(request) { - try { - const { event, data } = request.payload; - taskTestingEvents.emit(event, data); - return { event }; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'GET', - async handler() { - try { - return taskManager.fetch({ - query: taskManagerQuery, - }); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/task/{taskId}', - method: 'GET', - async handler(request) { - try { - await ensureIndexIsRefreshed(); - return await taskManager.get(request.params.taskId); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'DELETE', - async handler() { - try { - let tasksFound = 0; - do { - const { docs: tasks } = await taskManager.fetch({ - query: taskManagerQuery, - }); - tasksFound = tasks.length; - await Promise.all(tasks.map(task => taskManager.remove(task.id))); - } while (tasksFound > 0); - return 'OK'; - } catch (err) { - return err; - } - }, - }); -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json b/x-pack/test/plugin_api_integration/plugins/task_manager/package.json deleted file mode 100644 index ec63c512e9cd7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "sample_task_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "joi": "^13.5.2" - } -} diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index d664357c3ba12..d7bbc29bd861e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -7,7 +7,7 @@ import { merge, omit, times, chunk, isEmpty } from 'lodash'; import uuid from 'uuid'; import expect from '@kbn/expect/expect.js'; -import moment, { Moment } from 'moment'; +import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; import { IEvent } from '../../../../plugins/event_log/server'; import { IValidatedEvent } from '../../../../plugins/event_log/server/types'; @@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - describe('Event Log public API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64723 + // FLAKY: https://github.com/elastic/kibana/issues/64812 + describe.skip('Event Log public API', () => { it('should allow querying for events by Saved Object', async () => { const id = uuid.v4(); @@ -43,10 +45,8 @@ export default function({ getService }: FtrProviderContext) { it('should support pagination for events', async () => { const id = uuid.v4(); - const timestamp = moment(); - const [firstExpectedEvent, ...expectedEvents] = times(6, () => - fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) - ); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id)); + // run one first to create the SO and avoid clashes await logTestEvent(id, firstExpectedEvent); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); @@ -82,10 +82,7 @@ export default function({ getService }: FtrProviderContext) { it('should support sorting by event end', async () => { const id = uuid.v4(); - const timestamp = moment(); - const [firstExpectedEvent, ...expectedEvents] = times(6, () => - fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) - ); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id)); // run one first to create the SO and avoid clashes await logTestEvent(id, firstExpectedEvent); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); @@ -106,21 +103,24 @@ export default function({ getService }: FtrProviderContext) { it('should support date ranges for events', async () => { const id = uuid.v4(); - const timestamp = moment(); - - const firstEvent = fakeEvent(id, fakeEventTiming(timestamp)); + // write a document that shouldn't be found in the inclusive date range search + const firstEvent = fakeEvent(id); await logTestEvent(id, firstEvent); - await delay(100); - const start = timestamp.add(1, 's').toISOString(); + // wait a second, get the start time for the date range search + await delay(1000); + const start = new Date().toISOString(); - const expectedEvents = times(6, () => fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')))); + // write the documents that we should be found in the date range searches + const expectedEvents = times(6, () => fakeEvent(id)); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); - const end = timestamp.add(1, 's').toISOString(); + // get the end time for the date range search + const end = new Date().toISOString(); - await delay(100); - const lastEvent = fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))); + // write a document that shouldn't be found in the inclusive date range search + await delay(1000); + const lastEvent = fakeEvent(id); await logTestEvent(id, lastEvent); await retry.try(async () => { @@ -195,29 +195,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 2de395308ce74..31668e8345275 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 0000000000000..41f2cfc7983ef --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const notifyUsage = async (featureName: string, usedAt: number) => { + await supertest.get(`/api/feature_usage_test/hit?featureName=${featureName}&usedAt=${usedAt}`); + }; + + const toISO = (time: number) => new Date(time).toISOString(); + + describe('/api/licensing/feature_usage', () => { + it('returns a map of last feature usages', async () => { + const timeA = Date.now(); + await notifyUsage('test_feature_a', timeA); + + const timeB = Date.now() - 4567; + await notifyUsage('test_feature_b', timeB); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + + expect(response.body.test_feature_a).to.eql(toISO(timeA)); + expect(response.body.test_feature_b).to.eql(toISO(timeB)); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts new file mode 100644 index 0000000000000..6cafb60bf8167 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Licensed feature usage APIs', function() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_usage')); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index e8f976d5ae6e3..00cefa42711c9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -11,7 +11,7 @@ import supertestAsPromised from 'supertest-as-promised'; const { task: { properties: taskManagerIndexMapping }, -} = require('../../../../legacy/plugins/task_manager/server/mappings.json'); +} = require('../../../../plugins/task_manager/server/saved_objects/mappings.json'); const { DEFAULT_MAX_WORKERS, @@ -90,15 +90,6 @@ export default function({ getService }) { .then(response => response.body); } - function scheduleTaskUsingLegacyApi(task) { - return supertest - .post('/api/sample_tasks/schedule_legacy') - .set('kbn-xsrf', 'xxx') - .send({ task }) - .expect(200) - .then(response => response.body); - } - function runTaskNow(task) { return supertest .post('/api/sample_tasks/run_now') @@ -587,15 +578,5 @@ export default function({ getService }) { expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); }); }); - - it('should retain the legacy api until v8.0.0', async () => { - const result = await scheduleTaskUsingLegacyApi({ - id: 'task-with-legacy-api', - taskType: 'sampleTask', - params: {}, - }); - - expect(result.id).to.be('task-with-legacy-api'); - }); }); } diff --git a/x-pack/test/reporting/README.md b/x-pack/test/reporting/README.md deleted file mode 100644 index fc5147ad8c454..0000000000000 --- a/x-pack/test/reporting/README.md +++ /dev/null @@ -1,153 +0,0 @@ -## The Reporting Tests - -### Overview - -Reporting tests have their own top level test folder because: - - Current API tests run with `optimize.enabled=false` flag for performance reasons, but reporting actually requires UI assets. - - Reporting tests take a lot longer than other test types. This separation allows developers to run them in isolation, or to run other functional or API tests without them. - - ### Running the tests - - There is more information on running x-pack tests here: https://github.com/elastic/kibana/blob/master/x-pack/README.md#running-functional-tests. Similar to running the API tests, you need to specify a reporting configuration file. Reporting currently has two configuration files you can point to: - - test/reporting/configs/chromium_api.js - - test/reporting/configs/chromium_functional.js - - The `api` versions hit the reporting api and ensure report generation completes successfully, but does not verify the output of the reports. This is done in the `functional` test versions, which does a snapshot comparison of the generated URL against a baseline to determine success. - - To run the tests in a single command. : -1. cd into x-pack directory. -2. run: - ``` -node scripts/functional_tests --config test/reporting/configs/[config_file_name_here].js - ``` - - You can also run the test server seperately from the runner. This is beneficial when debugging as Kibana and Elasticsearch will remain up and running throughout multiple test runs. To do this: - -1. cd into x-pack directory. -2. In one terminal window, run: - ``` -node scripts/functional_tests_server.js --config test/reporting/configs/[test_config_name_here].js - ``` -3. In another terminal window, cd into x-pack dir and run: - ``` -node ../scripts/functional_test_runner.js --config test/reporting/configs/[test_config_name_here].js - ``` - -```sh -//OSX -brew install imagemagick ghostscript poppler - -//Ubutnu -sudo apt-get install imagemagick ghostscript poppler-utils -``` - -For windows: - -ImageMagick-6.9.9-37-Q16-HDRI-x64-dll.exe -from -https://sourceforge.net/projects/imagemagick/postdownload -Install with all default options - -gs925w64.exe -from -https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs925/gs925w64.exe -Install with all default options - - -**Note:** Configurations from `kibana.dev.yml` are picked up when running the tests. Ensure that `kibana.dev.yml` does not contain any `xpack.reporting` configurations. - -### Reporting baseline snapshots - -The functional version of the reporting tests create a few pdf reports and do a snapshot comparison against a couple baselines. The baseline images are stored in `./functional/reports/baseline`. - -#### Updating the baselines - -Every now and then visual changes will be made that will require the snapshots to be updated. This is how you go about updating it. I will discuss generating snapshots from chromium since that is the way of the future. - -1. Run the test server for chromium. - ``` -node scripts/functional_tests_server.js --config test/reporting/configs/chromium_functional.js - ``` - 2. Run the test runner - ``` - node ../scripts/functional_test_runner.js --config test/reporting/configs/chromium_functional.js - ``` - 3. This will create new report snapshots in `./functional/reports/session/`. - 4. After manually verifying the new reports in the `session` folder, copy them into [./functional/reports/baseline/](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/functional/reports/baseline) - 5. Create a new PR with the new snapshots in the baseline folder. - -**Note:** Dashboard has some snapshot testing too, in `_dashboard_snapshots.js`. This test watches for a command line flag `--updateBaselines` which automates updating the baselines. Probably worthwhile to do some similar here in the long run. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That loads the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/data -``` -^^ That loads the data indices. - -**Note:** Depending on your kibana.yml configuration, you may need to adjust the username, pw, and port in the urls above. - -5. Navigate to Kibana in the browser (`http://localhost:5601`) -6. Log in, pick any index to be the default to get page the management screen (doesn’t matter) -7. Generate some reporting URLs - - Use a mixture of Visualize, Discover (CSV), Dashboard - - Can view the current test coverage by checkout out [api/generation_urls.js](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/api/generation_urls.js). You can use different ones for better test coverage (e.g. different dashboards, different visualizations). - - Don’t generate urls from huge dashboards since this is time consuming. - - Use dashboards that have time saved with them if you wish to have data included. -8. Save these reporting urls. -9. Navigate back to the main branch via `git checkout master`. Then create, or work off your branch as usual to add the extra test coverage. -10. Copy the urls into `api/generation_urls.js`, stripping out the domain, port and base path (if they have it). -11. Write your new tests in [api/bwc_generation_urls.js](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/api/bwc_generation_urls.js) -12. Run tests via the mechanism above. - -**Note:** We may at some point wish to push most of these tests into an integration suite, as testing BWC of urls generated in every single minor, especially if there were not notable changes, may be overkill, especially given the time they add to the ci. - -### Expanding test coverage by including more data - -As Kibana development progresses, our existing data indices often fail to cover new situations, such as: - - Reports with new visualization types - - Canvas reports - - Reports with visualizations referencing new index types (e.g. visualizations referencing rolled up indices) - - etc - - Every now and then we should expand our test coverage to include new features. This is how you go about doing that in the context of reporting: - - 1. Checkout the `[version].x` branch, where `version` is the currently working minor version (e.g. 6.x, not `master`). This is because we don't want to run tests from data generated from a master version. The opposite works fine however - tests on master can run against data generated from the last minor. At least generally, though major version upgrades may require updating archived data (or be run through the appropriate migration scripts). - - 2. Load the current archives via: - ``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That loads the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/data -``` -^^ That loads the data indices. - -**Note:** Your es-url parameter might be different, but those are the default ports if running via `yarn start` and `yarn es snapshot --license trial`. - -3. Now generate the new data, create new index patterns, create new visualizations, and create new dashboards using those visualizations. All the fun stuff that you may want to use in your tests. - -**Note:** This data is used in open source dashboard testing. All visualizations and saved searches that have `Rendering Test` in their name are dynamically added to a new dashboard and their rendering is confirmed in https://github.com/elastic/kibana/tree/master/test/functional/apps/dashboard/_embedddable_rendering.js. You may need to adjust the expectations if you add new tests (which will be a good thing anyway, help extend the basic rendering tests - this way issues are caught before it gets to reporting tests!). Similarly all visualizations and saved searches that have `Filter Bytes Test` in their name are tested in https://github.com/elastic/kibana/tree/master/test/functional/apps/dashboard/_dashboard_filtering.js - -**Note:** The current reporting tests add visualizations from what is in `PageObjects.dashboard.getTestVisualizationNames`. We should probably instead use a saved dashboard we generate this report from. Then you can add any new visualizations, re-save the dashboard, and re-generate the snapshot above. - -4. After adding more visualizations to a test dashboard, update tests if necessary, update snapshots, then **save the new archives**! - ``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 save ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That saves the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 save ../../../../test/functional/fixtures/es_archiver/dashboard/current/data log* animal* dog* [ANY OTHER NEW INDEX NAME YOU ADDED]* -``` -^^ That saves the data indices. The last parameter is a list of indices you want archived. You don't want to include the `.kibana` one in there (this way you can use a custom `.kibana`, but can reuse the data archive, for tests, and `.kibana` archive is small, but the data archives are larger and should be reused). - -5. Create your PR with test updates, and the larger archive. - - - diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js deleted file mode 100644 index 753d2b2a20ab9..0000000000000 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export default async function({ readConfigFile }) { - // TODO move reporting tests to x-pack/test/functional/apps/<integration_app>/reporting - const functionalConfig = await readConfigFile(require.resolve('../../functional/config.js')); - - return { - services: functionalConfig.get('services'), - pageObjects: functionalConfig.get('pageObjects'), - servers: functionalConfig.get('servers'), - apps: functionalConfig.get('apps'), - screenshots: functionalConfig.get('screenshots'), - junit: { - reportName: 'X-Pack Chromium Functional Reporting Tests', - }, - testFiles: [require.resolve('../functional')], - kbnTestServer: { - ...functionalConfig.get('kbnTestServer'), - serverArgs: [ - ...functionalConfig.get('kbnTestServer.serverArgs'), - '--logging.events.log', - '["info","warning","error","fatal","optimize","reporting"]', - '--xpack.endpoint.enabled=true', - '--xpack.reporting.csv.enablePanelActionDownload=true', - '--xpack.reporting.csv.checkForFormulas=false', - '--xpack.reporting.csv.maxSizeBytes=25000000', - '--xpack.security.session.idleTimeout=3600000', - '--xpack.spaces.enabled=false', - ], - }, - esArchiver: functionalConfig.get('esArchiver'), - esTestCluster: functionalConfig.get('esTestCluster'), - security: { disableTestUser: true }, - }; -} diff --git a/x-pack/test/reporting/functional/index.js b/x-pack/test/reporting/functional/index.js deleted file mode 100644 index be3ac09bdec10..0000000000000 --- a/x-pack/test/reporting/functional/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export default function({ loadTestFile }) { - describe('reporting app', function() { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./reporting')); - }); -} diff --git a/x-pack/test/reporting/functional/lib/compare_pngs.js b/x-pack/test/reporting/functional/lib/compare_pngs.js deleted file mode 100644 index 16b1008c645a0..0000000000000 --- a/x-pack/test/reporting/functional/lib/compare_pngs.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import path from 'path'; -import fs from 'fs'; -import { promisify } from 'bluebird'; -import { comparePngs } from '../../../../../test/functional/services/lib/compare_pngs'; - -const mkdirAsync = promisify(fs.mkdir); - -export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) { - log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); - // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be - // stored. - const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); - const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); - - await mkdirAsync(sessionDirectoryPath, { recursive: true }); - await mkdirAsync(failureDirectoryPath, { recursive: true }); - - const actualpngFileName = path.basename(actualpngPath, '.png'); - const baselinepngFileName = path.basename(baselinepngPath, '.png'); - - const baselineCopyPath = path.resolve( - sessionDirectoryPath, - `${baselinepngFileName}_baseline.png` - ); - const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); - - // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we - // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have - // mac and linux covered which is better than nothing for now. - try { - log.debug(`writeFileSync: ${baselineCopyPath}`); - fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); - } catch (error) { - log.error(`No baseline png found at ${baselinepngPath}`); - return 0; - } - log.debug(`writeFileSync: ${actualCopyPath}`); - fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); - - let diffTotal = 0; - - const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); - diffTotal += await comparePngs( - actualCopyPath, - baselineCopyPath, - diffPngPath, - sessionDirectoryPath, - log - ); - - return diffTotal; -} diff --git a/x-pack/test/reporting/functional/lib/index.js b/x-pack/test/reporting/functional/lib/index.js deleted file mode 100644 index e7a08753b591f..0000000000000 --- a/x-pack/test/reporting/functional/lib/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { checkIfPngsMatch } from './compare_pngs'; diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js deleted file mode 100644 index c1a2ae634662c..0000000000000 --- 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 f348aa99e6f20..94e6a0a11aa99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4237,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== @@ -4798,6 +4798,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/set-value@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-2.0.0.tgz#63d386b103926dcf49b50e16e0f6dd49983046be" + integrity sha512-k8dCJEC80F/mbsIOZ5Hj3YSzTVVVBwMdtP/M9Rtc2TM4F5etVd+2UG8QUiAUfbXm4fABedL2tBZnrBheY7UwpA== + "@types/shot@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92" @@ -6838,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== @@ -26918,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"