diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index ebb9c3dc86dd2a..db19a0a5b249dc 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -12,8 +12,12 @@ kibanaPipeline(timeoutMinutes: 240) { ]) { workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { catchError { + + kibanaPipeline.bash(""" + echo '${TIME_STAMP}' + """, "### Print Canonical Time Stamp") + kibanaCoverage.runTests() - kibanaTeamAssign.load('team_assignment', "### Upload Team Assignment JSON") handleIngestion(TIME_STAMP) } handleFail() @@ -30,7 +34,7 @@ def handleIngestion(timestamp) { kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, '### Ingest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, teamAssignmentsPath(), '### Generate Team Assignments && Ingest') kibanaCoverage.uploadCoverageStaticSite(timestamp) } @@ -42,7 +46,7 @@ def handlePreviousSha() { def handleFail() { def buildStatus = buildUtils.getBuildStatus() - if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { + if (params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { slackNotifications.sendFailedBuild( channel: '#kibana-qa', username: 'Kibana QA' @@ -50,3 +54,7 @@ def handleFail() { } } +def teamAssignmentsPath() { + return 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt' +} + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f5e14f1f15991..54cf52fae5fe8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,9 @@ # Identify which groups will be pinged by changes to different parts of the codebase. # For more info, see https://help.github.com/articles/about-codeowners/ +# The #CC# prefix delineates Code Coverage, +# used for the 'team' designator within Kibana Stats + # App /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/discover_enhanced/ @elastic/kibana-app @@ -26,6 +29,33 @@ /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app +#CC# /src/plugins/vis_type @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/common/utils @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/migrations @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/console_legacy @elastic/kibana-app +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app +#CC# /src/legacy/core_plugins/timelion @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_tagcloud @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_vega @elastic/kibana-app +#CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +#CC# /src/legacy/server/sample_data/ @elastic/kibana-app +#CC# /src/legacy/server/url_shortening/ @elastic/kibana-app +#CC# /src/legacy/ui/public/state_management @elastic/kibana-app +#CC# /src/plugins/charts/public/static/color_maps @elastic/kibana-app +#CC# /src/plugins/index_pattern_management/public @elastic/kibana-app +#CC# /src/plugins/input_control_vis/ @elastic/kibana-app +#CC# /src/plugins/kibana_legacy/ @elastic/kibana-app +#CC# /src/plugins/timelion @elastic/kibana-app +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app +#CC# /x-pack/plugins/lens/ @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -56,12 +86,37 @@ /x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch +#CC# /src/legacy/core_plugins/status_page/public @elastic/kibana-app-arch +#CC# /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +#CC# /src/legacy/ui/public/field_editor @elastic/kibana-app-arch +#CC# /src/legacy/ui/public/management @elastic/kibana-app-arch +#CC# /src/plugins/advanced_settings/ @elastic/kibana-app-arch +#CC# /src/plugins/bfetch/ @elastic/kibana-app-arch +#CC# /src/plugins/charts/ @elastic/kibana-app-arch +#CC# /src/plugins/index_pattern_management/public/service @elastic/kibana-app-arch +#CC# /src/plugins/inspector/ @elastic/kibana-app-arch +#CC# /src/plugins/saved_objects/ @elastic/kibana-app-arch +#CC# /src/plugins/share/ @elastic/kibana-app-arch +#CC# /src/plugins/vis_default_editor @elastic/kibana-app-arch +#CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +#CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch +#CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam +#CC# /src/plugins/apm_oss/ @elastic/apm-ui +#CC# /src/legacy/core_plugins/apm_oss/ @elastic/apm-ui +#CC# /src/legacy/ui/public/apm @elastic/apm-ui +#CC# /x-pack/legacy/plugins/apm/ @elastic/apm-ui +#CC# /x-pack/plugins/observability/ @elastic/apm-ui # Client Side Monitoring (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime @@ -71,13 +126,19 @@ /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime /x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime +/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime +#CC# /x-pack/legacy/plugins/uptime @elastic/uptime # Beats /x-pack/plugins/beats_management/ @elastic/beats +/x-pack/legacy/plugins/beats_management/ @elastic/beats +#CC# /x-pack/plugins/beats_management/ @elastic/beats # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas +#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas +#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -85,6 +146,12 @@ /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui +#CC# /src/legacy/core_plugins/newsfeed @elastic/kibana-core-ui +#CC# /src/plugins/newsfeed @elastic/kibana-core-ui +#CC# /src/plugins/home/public @elastic/kibana-core-ui +#CC# /src/plugins/home/server/services/ @elastic/kibana-core-ui +#CC# /src/plugins/home/ @elastic/kibana-core-ui +#CC# /x-pack/plugins/global_search_providers/ @elastic/kibana-core-ui # Observability UIs /x-pack/plugins/infra/ @elastic/logs-metrics-ui @@ -110,6 +177,14 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis +#CC# /src/legacy/core_plugins/region_map @elastic/kibana-gis +#CC# /src/legacy/core_plugins/tile_map @elastic/kibana-gis +#CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +#CC# /x-pack/plugins/file_upload @elastic/kibana-gis +#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis +#CC# /src/plugins/home/server/tutorials @elastic/kibana-gis +#CC# /src/plugins/tile_map/ @elastic/kibana-gis +#CC# /src/plugins/region_map/ @elastic/kibana-gis # Operations /src/dev/ @elastic/kibana-operations @@ -132,6 +207,7 @@ /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations +#CC# /packages/kbn-expect/ @elastic/kibana-operations # Quality Assurance /src/dev/code_coverage @elastic/kibana-qa @@ -158,6 +234,31 @@ /src/plugins/status_page/ @elastic/kibana-platform /src/plugins/saved_objects_management/ @elastic/kibana-platform /src/dev/run_check_published_api_changes.ts @elastic/kibana-platform +#CC# /src/core/server/csp/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/lib @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/lib/management/saved_objects @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/routes/api/import/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/kibana/server/routes/api/export/ @elastic/kibana-platform +#CC# /src/legacy/core_plugins/elasticsearch @elastic/kibana-platform +#CC# /src/legacy/core_plugins/testbed @elastic/kibana-platform +#CC# /src/legacy/server/config/ @elastic/kibana-platform +#CC# /src/legacy/server/http/ @elastic/kibana-platform +#CC# /src/legacy/server/status/ @elastic/kibana-platform +#CC# /src/legacy/ui/public/new_platform @elastic/kibana-platform +#CC# /src/legacy/ui/public/plugin_discovery @elastic/kibana-platform +#CC# /src/legacy/ui/public/chrome @elastic/kibana-platform +#CC# /src/legacy/ui/public/notify @elastic/kibana-platform +#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform +#CC# /src/legacy/ui/public/autoload @elastic/kibana-platform +#CC# /src/plugins/legacy_export/ @elastic/kibana-platform +#CC# /src/plugins/status_page/ @elastic/kibana-platform +#CC# /src/plugins/testbed/server/ @elastic/kibana-platform +#CC# /x-pack/legacy/plugins/xpack_main/server/ @elastic/kibana-platform +#CC# /x-pack/legacy/server/lib/ @elastic/kibana-platform +#CC# /x-pack/plugins/cloud/ @elastic/kibana-platform +#CC# /x-pack/plugins/features/ @elastic/kibana-platform +#CC# /x-pack/plugins/global_search/ @elastic/kibana-platform +#CC# /src/legacy/plugin_discovery/ @elastic/kibana-platform # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform @@ -177,12 +278,19 @@ /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security /x-pack/test/token_api_integration/ @elastic/kibana-security +#CC# /src/legacy/ui/public/capabilities @elastic/kibana-security +#CC# /x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security +#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security +#CC# /x-pack/plugins/security/ @elastic/kibana-security +#CC# /x-pack/plugins/audit_trail/ @elastic/kibana-security # Kibana Localization /src/dev/i18n/ @elastic/kibana-localization /src/legacy/server/i18n/ @elastic/kibana-localization /src/core/public/i18n/ @elastic/kibana-localization /packages/kbn-i18n/ @elastic/kibana-localization +#CC# /src/legacy/server/i18n/ @elastic/kibana-localization +#CC# /x-pack/plugins/translations/ @elastic/kibana-localization # Kibana Telemetry /packages/kbn-analytics/ @elastic/kibana-telemetry @@ -211,6 +319,11 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /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 +#CC# /x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/task_manager @elastic/kibana-alerting-services +#CC# /x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services +#CC# /x-pack/plugins/alerting_builtins @elastic/kibana-alerting-services # Enterprise Search # Shared @@ -248,6 +361,12 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/ingest_pipelines/ @elastic/es-ui /packages/kbn-ace/ @elastic/es-ui /packages/kbn-monaco/ @elastic/es-ui +#CC# /x-pack/legacy/plugins/rollup/ @elastic/es-ui +#CC# /x-pack/legacy/server/lib/create_router/ @elastic/es-ui +#CC# /x-pack/legacy/server/lib/check_license/ @elastic/es-ui +#CC# /x-pack/plugins/console_extensions/ @elastic/es-ui +#CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui +#CC# /x-pack/plugins/es_ui_shared/ @elastic/es-u # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem @@ -257,6 +376,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /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 +#CC# /x-pack/legacy/plugins/siem/ @elastic/siem +#CC# /x-pack/plugins/siem/ @elastic/siem +#CC# /x-pack/plugins/security_solution/ @elastic/siem # Security Solution /x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team @@ -271,6 +393,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design +#CC# /packages/kbn-ui-framework/ @elastic/kibana-design # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers @@ -292,3 +415,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/endpoint/**/*.scss @elastic/security-design /x-pack/plugins/security_solution/**/*.scss @elastic/security-design +# Logstash +#CC# /x-pack/plugins/logstash/ @elastic/logstash + +# Reporting +#CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + diff --git a/examples/alerting_example/public/alert_types/index.ts b/examples/alerting_example/public/alert_types/index.ts deleted file mode 100644 index db9f855b573e85..00000000000000 --- a/examples/alerting_example/public/alert_types/index.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 { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; -import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; - -export function registerNavigation(alerts: AlertingSetup) { - // register default navigation - alerts.registerDefaultNavigation( - ALERTING_EXAMPLE_APP_ID, - (alert: SanitizedAlert) => `/alert/${alert.id}` - ); - - registerPeopleInSpaceNavigation(alerts); -} diff --git a/examples/alerting_example/public/index.ts b/examples/alerting_example/public/index.ts deleted file mode 100644 index 4a2bfc79903c33..00000000000000 --- a/examples/alerting_example/public/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AlertingExamplePlugin } from './plugin'; - -export const plugin = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/server/alert_types/always_firing.ts b/examples/alerting_example/server/alert_types/always_firing.ts deleted file mode 100644 index b89e5da0893369..00000000000000 --- a/examples/alerting_example/server/alert_types/always_firing.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 uuid from 'uuid'; -import { range } from 'lodash'; -import { AlertType } from '../../../../x-pack/plugins/alerts/server'; -import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; - -export const alertType: AlertType = { - id: 'example.always-firing', - name: 'Always firing', - actionGroups: [{ id: 'default', name: 'default' }], - defaultActionGroupId: 'default', - async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { - const count = (state.count ?? 0) + 1; - - range(instances) - .map(() => ({ id: uuid.v4() })) - .forEach((instance: { id: string }) => { - services - .alertInstanceFactory(instance.id) - .replaceState({ triggerdOnCycle: count }) - .scheduleActions('default'); - }); - - return { - count, - }; - }, - producer: ALERTING_EXAMPLE_APP_ID, -}; diff --git a/examples/alerting_example/server/index.ts b/examples/alerting_example/server/index.ts deleted file mode 100644 index 32e9b181ebb544..00000000000000 --- a/examples/alerting_example/server/index.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. - */ - -import { PluginInitializer } from 'kibana/server'; -import { AlertingExamplePlugin } from './plugin'; - -export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); diff --git a/package.json b/package.json index 505fac1df935ee..9b1735f6f7e5e3 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "check-disk-space": "^2.1.0", "chokidar": "3.2.1", "color": "1.0.3", - "commander": "3.0.2", + "commander": "^3.0.2", "core-js": "^3.6.4", "cypress-promise": "^1.1.0", "deep-freeze-strict": "^1.1.1", @@ -184,12 +184,12 @@ "json-stable-stringify": "^1.0.1", "json-stringify-safe": "5.0.1", "lodash": "^4.17.20", - "lru-cache": "4.1.5", + "lru-cache": "^4.1.5", "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "mustache": "2.3.2", - "node-fetch": "2.6.1", + "mustache": "^2.3.2", + "node-fetch": "^2.6.1", "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", @@ -212,7 +212,7 @@ "rison-node": "1.0.2", "rxjs": "^6.5.5", "seedrandom": "^3.0.5", - "semver": "^5.5.0", + "semver": "^5.7.0", "style-it": "^2.1.3", "symbol-observable": "^1.2.0", "tar": "4.4.13", @@ -223,7 +223,7 @@ "uuid": "3.3.2", "vision": "^5.3.3", "whatwg-fetch": "^3.0.0", - "yauzl": "2.10.0" + "yauzl": "^2.10.0" }, "devDependencies": { "@babel/parser": "^7.11.2", @@ -279,7 +279,7 @@ "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", "@types/getos": "^3.0.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", "@types/h2o2": "^8.1.1", @@ -312,7 +312,7 @@ "@types/normalize-path": "^3.0.0", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", - "@types/pngjs": "^3.3.2", + "@types/pngjs": "^3.4.0", "@types/podium": "^1.0.0", "@types/prop-types": "^15.7.3", "@types/reach__router": "^1.2.6", @@ -393,7 +393,7 @@ "fetch-mock": "^7.3.9", "fp-ts": "^2.3.1", "geckodriver": "^1.20.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "grunt": "1.0.4", "grunt-available-tasks": "^0.6.3", "grunt-cli": "^1.2.0", @@ -439,7 +439,7 @@ "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", - "ngreact": "0.5.1", + "ngreact": "^0.5.1", "nock": "12.0.3", "normalize-path": "^3.0.0", "nyc": "^15.0.1", diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json index cf74d745f4cae9..6f1cee764adf10 100644 --- a/packages/kbn-ace/package.json +++ b/packages/kbn-ace/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "typescript": "4.0.2" } } diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 2d9dbc3b7ab8f7..062520f47f0f93 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -25,6 +25,6 @@ }, "devDependencies": { "typescript": "4.0.2", - "tsd": "^0.7.4" + "tsd": "^0.13.1" } } diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index a3fe8178822aa5..a85f5924f0ea20 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -12,7 +12,7 @@ "dependencies": { "@babel/core": "^7.11.1", "@kbn/utils": "1.0.0", - "axios": "^0.19.0", + "axios": "^0.19.2", "chalk": "^4.1.0", "cheerio": "0.22.0", "dedent": "^0.7.0", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 52ef3fe05e751f..40d34c5d710bb2 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -1,23 +1,32 @@ { "name": "@kbn/es", - "main": "./src/index.js", + "main": "./target/index.js", "version": "1.0.0", "license": "Apache-2.0", "private": true, + "scripts": { + "kbn:bootstrap": "node scripts/build", + "kbn:watch": "node scripts/build --watch" + }, "dependencies": { "@elastic/elasticsearch": "7.9.0-rc.1", "@kbn/dev-utils": "1.0.0", - "abort-controller": "^2.0.3", + "abort-controller": "^3.0.0", "chalk": "^4.1.0", "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "node-fetch": "^2.6.1", - "simple-git": "^1.91.0", + "simple-git": "1.116.0", "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", "yauzl": "^2.10.0" + }, + "devDependencies": { + "@kbn/babel-preset": "1.0.0", + "@babel/cli": "^7.10.5", + "del": "^5.1.0" } } diff --git a/packages/kbn-es/scripts/build.js b/packages/kbn-es/scripts/build.js new file mode 100644 index 00000000000000..50aad665c920ba --- /dev/null +++ b/packages/kbn-es/scripts/build.js @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + log.info('Deleting old output'); + await del(BUILD_DIR); + + const cwd = ROOT_DIR; + + log.info(`Starting babel${flags.watch ? ' in watch mode' : ''}`); + await proc.run(`babel`, { + cmd: 'babel', + args: [ + 'src', + '--no-babelrc', + '--presets', + require.resolve('@kbn/babel-preset/node_preset'), + '--extensions', + '.ts,.js', + '--copy-files', + '--out-dir', + BUILD_DIR, + ...(flags.watch ? ['--watch'] : ['--quiet']), + ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE + ? [] + : ['--source-maps', 'inline']), + ], + wait: true, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for @kbn/es package', + flags: { + boolean: ['watch', 'source-maps'], + help: ` + --watch Run in watch mode + --source-maps Include sourcemaps + `, + }, + } +); diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index b860664443d1ab..27e73e6c204e88 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -25,65 +25,67 @@ const { exitCode, start, ssl } = JSON.parse(process.argv[2]); const { createServer } = ssl ? require('https') : require('http'); const { ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); -process.exitCode = exitCode; +(function main() { + process.exitCode = exitCode; -if (!start) { - return; -} + if (!start) { + return; + } + + let serverUrl; + const server = createServer( + { + // Note: the integration uses the ES_P12_PATH, but that keystore contains + // the same key/cert as ES_KEY_PATH and ES_CERT_PATH + key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, + cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, + }, + (req, res) => { + const url = new URL(req.url, serverUrl); + const send = (code, body) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(body)); + }; -let serverUrl; -const server = createServer( - { - // Note: the integration uses the ES_P12_PATH, but that keystore contains - // the same key/cert as ES_KEY_PATH and ES_CERT_PATH - key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, - cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, - }, - (req, res) => { - const url = new URL(req.url, serverUrl); - const send = (code, body) => { - res.writeHead(code, { 'content-type': 'application/json' }); - res.end(JSON.stringify(body)); - }; + if (url.pathname === '/_xpack') { + return send(400, { + error: { + reason: 'foo bar', + }, + }); + } - if (url.pathname === '/_xpack') { - return send(400, { + return send(404, { error: { - reason: 'foo bar', + reason: 'not found', }, }); } + ); - return send(404, { - error: { - reason: 'not found', - }, - }); - } -); - -// setup server auto close after 1 second of silence -let serverCloseTimer; -const delayServerClose = () => { - clearTimeout(serverCloseTimer); - serverCloseTimer = setTimeout(() => server.close(), 1000); -}; -server.on('request', delayServerClose); -server.on('listening', delayServerClose); + // setup server auto close after 1 second of silence + let serverCloseTimer; + const delayServerClose = () => { + clearTimeout(serverCloseTimer); + serverCloseTimer = setTimeout(() => server.close(), 1000); + }; + server.on('request', delayServerClose); + server.on('listening', delayServerClose); -server.listen(0, '127.0.0.1', function () { - const { port, address: hostname } = server.address(); - serverUrl = new URL( - formatUrl({ - protocol: 'http:', - port, - hostname, - }) - ); + server.listen(0, '127.0.0.1', function () { + const { port, address: hostname } = server.address(); + serverUrl = new URL( + formatUrl({ + protocol: 'http:', + port, + hostname, + }) + ); - console.log( - `[o.e.h.AbstractHttpServerTransport] [computer] publish_address {127.0.0.1:${port}}, bound_addresses {[::1]:${port}}, {127.0.0.1:${port}}` - ); + console.log( + `[o.e.h.AbstractHttpServerTransport] [computer] publish_address {127.0.0.1:${port}}, bound_addresses {[::1]:${port}}, {127.0.0.1:${port}}` + ); - console.log('started'); -}); + console.log('started'); + }); +})(); diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 332f7e8a20cc2e..223c73e97908e5 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -13,7 +13,7 @@ "debug": "^2.6.9", "eslint-import-resolver-node": "0.3.2", "eslint-import-resolver-webpack": "0.11.1", - "glob-all": "^3.1.0", + "glob-all": "^3.2.1", "lru-cache": "^4.1.5", "resolve": "^1.7.1", "webpack": "^4.41.5" diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index eccdff9060cbe8..9e0ec50fb838e3 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -19,7 +19,7 @@ "@types/intl-relativeformat": "^2.1.0", "@types/react-intl": "^2.3.15", "del": "^5.1.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "supports-color": "^7.0.0", "typescript": "4.0.2" }, diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index aef63229ebe962..430dac6cb2e00b 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -11,7 +11,7 @@ "dependencies": { "@babel/runtime": "^7.11.2", "@kbn/i18n": "1.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "uuid": "3.3.2" }, "devDependencies": { @@ -25,12 +25,12 @@ "copy-webpack-plugin": "^6.0.2", "css-loader": "^3.4.2", "del": "^5.1.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "pegjs": "0.10.0", "sass-loader": "^8.0.2", "style-loader": "^1.1.3", "supports-color": "^7.0.0", - "url-loader": "2.2.0", + "url-loader": "^2.2.0", "webpack": "^4.41.5", "webpack-cli": "^3.3.10" } diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index ca133010fe2304..fcea80c9b71106 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -18,7 +18,7 @@ "babel-loader": "^8.0.6", "css-loader": "^3.4.2", "del": "^5.1.0", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "supports-color": "^7.0.0", "typescript": "4.0.2", "webpack": "^4.41.5", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b80d1365659dda..f90fcaec79fe0d 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -27,7 +27,7 @@ "jest-diff": "^25.5.0", "json-stable-stringify": "^1.0.1", "loader-utils": "^1.2.3", - "node-sass": "^4.13.0", + "node-sass": "^4.13.1", "normalize-path": "^3.0.0", "postcss": "^7.0.32", "postcss-loader": "^3.0.0", diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 45598ff8831b0b..b1ab1ebfe49f23 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -161,7 +161,8 @@ export class OptimizerConfig { Path.resolve(repoRoot, 'src/plugins'), ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), Path.resolve(repoRoot, 'plugins'), - ...(examples ? [Path.resolve('examples'), Path.resolve('x-pack/examples')] : []), + ...(examples ? [Path.resolve('examples')] : []), + ...(examples && !oss ? [Path.resolve('x-pack/examples')] : []), Path.resolve(repoRoot, '../kibana-extra'), ]; if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) { diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9f2c5654a8bd40..2edf1c999888ee 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -216,7 +216,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: }, resolve: { - extensions: ['.js', '.ts', '.tsx', 'json'], + extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index f292387c125219..65b44b6965048d 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@types/extract-zip": "^1.6.2", "@types/gulp-zip": "^4.0.1", - "@types/inquirer": "^6.5.0", + "@types/inquirer": "^7.3.1", "extract-zip": "^2.0.1", "typescript": "4.0.2" } diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 1fb94e4c92ce13..c2f9236d9e7984 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -19,8 +19,8 @@ "@types/cpy": "^5.1.0", "@types/dedent": "^0.7.0", "@types/getopts": "^2.0.1", - "@types/glob": "^5.0.35", - "@types/globby": "^6.1.0", + "@types/glob": "^7.1.2", + "@types/globby": "^8.0.0", "@types/has-ansi": "^3.0.0", "@types/lodash": "^4.14.159", "@types/log-symbols": "^2.0.0", @@ -41,12 +41,12 @@ "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "globby": "^8.0.1", "has-ansi": "^3.0.0", "is-path-inside": "^3.0.2", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", "ncp": "^2.0.0", diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 63ce93ac54f463..0c80c949c1d11c 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -21,7 +21,7 @@ "prettier": "^2.1.1" }, "dependencies": { - "commander": "^3.0.0", + "commander": "^3.0.2", "glob": "^7.1.2" } -} \ No newline at end of file +} diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index 2cc9fd72082be5..a931dd3f3154d6 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -15,6 +15,6 @@ }, "dependencies": { "@kbn/utility-types": "1.0.0", - "lodash": "^4.17.15" + "lodash": "^4.17.20" } } diff --git a/packages/kbn-std/src/merge.ts b/packages/kbn-std/src/merge.ts index dca80774356576..43878c27b1e199 100644 --- a/packages/kbn-std/src/merge.ts +++ b/packages/kbn-std/src/merge.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isPlainObject from 'lodash/isPlainObject'; +import { isPlainObject } from 'lodash'; /** * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays. * diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 5271ddb96d8422..30ab0c701b295a 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -22,10 +22,10 @@ "jest-specific-snapshot": "2.0.0", "jest-styled-components": "^7.0.2", "mkdirp": "0.5.1", - "mini-css-extract-plugin": "0.7.0", - "normalize-path": "3.0.0", - "react-docgen-typescript-loader": "3.1.0", - "rxjs": "6.5.5", + "mini-css-extract-plugin": "0.8.0", + "normalize-path": "^3.0.0", + "react-docgen-typescript-loader": "^3.1.1", + "rxjs": "^6.5.5", "serve-static": "1.14.1", "styled-components": "^5.1.0", "webpack": "^4.41.5" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts new file mode 100644 index 00000000000000..833344fa368b03 --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SyntaxKind } from 'typescript'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const parsedSchemaDefinedWithSpreadsCollector: ParsedUsageCollection = [ + 'src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts', + { + collectorName: 'schema_defined_with_spreads', + schema: { + value: { + flat: { + type: 'keyword', + }, + my_str: { + type: 'text', + }, + my_objects: { + total: { + type: 'number', + }, + type: { + type: 'boolean', + }, + }, + }, + }, + fetch: { + typeName: 'Usage', + typeDescriptor: { + flat: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, + my_str: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, + my_objects: { + total: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + type: { + kind: SyntaxKind.BooleanKeyword, + type: 'BooleanKeyword', + }, + }, + }, + }, + }, +]; diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 206f573b0af782..4725be77533af1 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -142,6 +142,53 @@ Array [ }, }, ], + Array [ + "src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts", + Object { + "collectorName": "schema_defined_with_spreads", + "fetch": Object { + "typeDescriptor": Object { + "flat": Object { + "kind": 146, + "type": "StringKeyword", + }, + "my_objects": Object { + "total": Object { + "kind": 143, + "type": "NumberKeyword", + }, + "type": Object { + "kind": 131, + "type": "BooleanKeyword", + }, + }, + "my_str": Object { + "kind": 146, + "type": "StringKeyword", + }, + }, + "typeName": "Usage", + }, + "schema": Object { + "value": Object { + "flat": Object { + "type": "keyword", + }, + "my_objects": Object { + "total": Object { + "type": "number", + }, + "type": Object { + "type": "boolean", + }, + }, + "my_str": Object { + "type": "text", + }, + }, + }, + }, + ], Array [ "src/fixtures/telemetry_collectors/working_collector.ts", Object { diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts index 0517cb9034d0aa..b03db75b219f63 100644 --- a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts @@ -34,7 +34,7 @@ describe('extractCollectors', () => { const programPaths = await getProgramPaths(configs[0]); const results = [...extractCollectors(programPaths, tsConfig)]; - expect(results).toHaveLength(7); + expect(results).toHaveLength(8); expect(results).toMatchSnapshot(); }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts index b7ca33a7bcd743..d036b93a7bbf96 100644 --- a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts @@ -25,6 +25,7 @@ import { parsedNestedCollector } from './__fixture__/parsed_nested_collector'; import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector'; import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface'; import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema'; +import { parsedSchemaDefinedWithSpreadsCollector } from './__fixture__/parsed_schema_defined_with_spreads_collector'; export function loadFixtureProgram(fixtureName: string) { const fixturePath = path.resolve( @@ -62,6 +63,12 @@ describe('parseUsageCollection', () => { expect(result).toEqual([parsedWorkingCollector]); }); + it('parses collector with schema defined as union of spreads', () => { + const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector'); + const result = [...parseUsageCollection(sourceFile, program)]; + expect(result).toEqual([parsedSchemaDefinedWithSpreadsCollector]); + }); + it('parses nested collectors', () => { const { program, sourceFile } = loadFixtureProgram('nested_collector'); const result = [...parseUsageCollection(sourceFile, program)]; diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index e8e1b3fed1aef9..ac6edcb363fb66 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -100,42 +100,55 @@ export function getIdentifierDeclaration(node: ts.Node) { return getIdentifierDeclarationFromSource(node, source); } -export function getVariableValue(node: ts.Node): string | Record { +export function getVariableValue(node: ts.Node, program: ts.Program): string | Record { if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { return node.text; } if (ts.isObjectLiteralExpression(node)) { - return serializeObject(node); + return serializeObject(node, program); } if (ts.isIdentifier(node)) { const declaration = getIdentifierDeclaration(node); if (ts.isVariableDeclaration(declaration) && declaration.initializer) { - return getVariableValue(declaration.initializer); + return getVariableValue(declaration.initializer, program); + } else { + // Go fetch it in another file + return getIdentifierValue(node, node, program, { chaseImport: true }); } - // TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue } - throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`); + if (ts.isSpreadAssignment(node)) { + return getVariableValue(node.expression, program); + } + + throw Error( + `Unsupported Node: cannot get value of node (${node.getText()}) of kind ${node.kind}` + ); } -export function serializeObject(node: ts.Node) { +export function serializeObject(node: ts.Node, program: ts.Program) { if (!ts.isObjectLiteralExpression(node)) { throw new Error(`Expecting Object literal Expression got ${node.getText()}`); } - const value: Record = {}; + let value: Record = {}; for (const property of node.properties) { const propertyName = property.name?.getText(); + const val = ts.isPropertyAssignment(property) + ? getVariableValue(property.initializer, program) + : getVariableValue(property, program); + if (typeof propertyName === 'undefined') { - throw new Error(`Unable to get property name ${property.getText()}`); - } - const cleanPropertyName = propertyName.replace(/["']/g, ''); - if (ts.isPropertyAssignment(property)) { - value[cleanPropertyName] = getVariableValue(property.initializer); + if (typeof val === 'object') { + value = { ...value, ...val }; + } else { + throw new Error(`Unable to get property name ${property.getText()}`); + } } else { - value[cleanPropertyName] = getVariableValue(property); + const cleanPropertyName = propertyName.replace(/["']/g, ''); + value[cleanPropertyName] = val; } } @@ -155,45 +168,53 @@ export function getResolvedModuleSourceFile( return resolvedModuleSourceFile; } -export function getPropertyValue( +export function getIdentifierValue( node: ts.Node, + initializer: ts.Identifier, program: ts.Program, config: Optional<{ chaseImport: boolean }> = {} ) { const { chaseImport = false } = config; + const identifierName = initializer.getText(); + const declaration = getIdentifierDeclaration(initializer); + if (ts.isImportSpecifier(declaration)) { + if (!chaseImport) { + throw new Error( + `Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.` + ); + } - if (ts.isPropertyAssignment(node)) { - const { initializer } = node; + const importedModuleName = getModuleSpecifier(declaration); - if (ts.isIdentifier(initializer)) { - const identifierName = initializer.getText(); - const declaration = getIdentifierDeclaration(initializer); - if (ts.isImportSpecifier(declaration)) { - if (!chaseImport) { - throw new Error( - `Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.` - ); - } + const source = node.getSourceFile(); + const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName); + const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource); + if (!ts.isVariableDeclaration(declarationNode)) { + throw new Error(`Expected ${identifierName} to be variable declaration.`); + } + if (!declarationNode.initializer) { + throw new Error(`Expected ${identifierName} to be initialized.`); + } + const serializedObject = serializeObject(declarationNode.initializer, program); + return serializedObject; + } - const importedModuleName = getModuleSpecifier(declaration); + return getVariableValue(declaration, program); +} - const source = node.getSourceFile(); - const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName); - const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource); - if (!ts.isVariableDeclaration(declarationNode)) { - throw new Error(`Expected ${identifierName} to be variable declaration.`); - } - if (!declarationNode.initializer) { - throw new Error(`Expected ${identifierName} to be initialized.`); - } - const serializedObject = serializeObject(declarationNode.initializer); - return serializedObject; - } +export function getPropertyValue( + node: ts.Node, + program: ts.Program, + config: Optional<{ chaseImport: boolean }> = {} +) { + if (ts.isPropertyAssignment(node)) { + const { initializer } = node; - return getVariableValue(declaration); + if (ts.isIdentifier(initializer)) { + return getIdentifierValue(node, initializer, program, config); } - return getVariableValue(initializer); + return getVariableValue(initializer, program); } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8a4ff55dcf68fa..c616c836d5ff49 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -26,15 +26,14 @@ "dedent": "^0.7.0", "del": "^5.1.0", "exit-hook": "^2.2.0", - "getopts": "^2.2.4", + "getopts": "^2.2.5", "glob": "^7.1.2", "joi": "^13.5.2", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^5.2.0", "tar-fs": "^2.1.0", - "tmp": "^0.1.0", "xml2js": "^0.4.22", "zlib": "^1.0.5" } diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 363f97522a9018..1f916c58e2ecd2 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -17,7 +17,7 @@ "dependencies": { "classnames": "2.2.6", "focus-trap-react": "^3.1.1", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "prop-types": "^15.7.2", "react": "^16.12.0", "react-ace": "^5.9.0", @@ -31,7 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.11.1", - "@elastic/eui": "0.0.55", + "@elastic/eui": "29.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", @@ -62,7 +62,7 @@ "react-router": "^3.2.0", "react-router-redux": "^4.0.8", "redux": "3.7.2", - "redux-thunk": "2.2.0", + "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.3", "sass-loader": "^8.0.2", "sinon": "^7.4.2", diff --git a/rfcs/text/0013_saved_object_migrations.md b/rfcs/text/0013_saved_object_migrations.md new file mode 100644 index 00000000000000..c5069625cb8a66 --- /dev/null +++ b/rfcs/text/0013_saved_object_migrations.md @@ -0,0 +1,745 @@ +- Start Date: 2020-05-11 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +--- +- [1. Summary](#1-summary) +- [2. Motivation](#2-motivation) +- [3. Saved Object Migration Errors](#3-saved-object-migration-errors) +- [4. Design](#4-design) + - [4.0 Assumptions and tradeoffs](#40-assumptions-and-tradeoffs) + - [4.1 Discover and remedy potential failures before any downtime](#41-discover-and-remedy-potential-failures-before-any-downtime) + - [4.2 Automatically retry failed migrations until they succeed](#42-automatically-retry-failed-migrations-until-they-succeed) + - [4.2.1 Idempotent migrations performed without coordination](#421-idempotent-migrations-performed-without-coordination) + - [4.2.1.1 Restrictions](#4211-restrictions) + - [4.2.1.2 Migration algorithm: Cloned index per version](#4212-migration-algorithm-cloned-index-per-version) + - [4.2.1.3 Upgrade and rollback procedure](#4213-upgrade-and-rollback-procedure) + - [4.2.1.4 Handling documents that belong to a disabled plugin](#4214-handling-documents-that-belong-to-a-disabled-plugin) +- [5. Alternatives](#5-alternatives) + - [5.1 Rolling upgrades](#51-rolling-upgrades) + - [5.2 Single node migrations coordinated through a lease/lock](#52-single-node-migrations-coordinated-through-a-leaselock) + - [5.2.1 Migration algorithm](#521-migration-algorithm) + - [5.2.2 Document lock algorithm](#522-document-lock-algorithm) + - [5.2.3 Checking for "weak lease" expiry](#523-checking-for-weak-lease-expiry) + - [5.3 Minimize data loss with mixed Kibana versions during 7.x](#53-minimize-data-loss-with-mixed-kibana-versions-during-7x) + - [5.4 In-place migrations that re-use the same index (8.0)](#54-in-place-migrations-that-re-use-the-same-index-80) + - [5.4.1 Migration algorithm (8.0):](#541-migration-algorithm-80) + - [5.4.2 Minimizing data loss with unsupported upgrade configurations (8.0)](#542-minimizing-data-loss-with-unsupported-upgrade-configurations-80) + - [5.5 Tag objects as “invalid” if their transformation fails](#55-tag-objects-as-invalid-if-their-transformation-fails) +- [6. How we teach this](#6-how-we-teach-this) +- [7. Unresolved questions](#7-unresolved-questions) + +# 1. Summary + +Improve the Saved Object migration algorithm to ensure a smooth Kibana upgrade +procedure. + +# 2. Motivation + +Kibana version upgrades should have a minimal operational impact. To achieve +this, users should be able to rely on: + +1. A predictable downtime window. +2. A small downtime window. + 1. (future) provide a small downtime window on indices with 10k or even + a 100k documents. +3. The ability to discover and remedy potential failures before initiating the + downtime window. +4. Quick roll-back in case of failure. +5. Detailed documentation about the impact of downtime on the features they + are using (e.g. actions, task manager, fleet, reporting). +6. Mixed Kibana versions shouldn’t cause data loss. +7. (stretch goal) Maintain read-only functionality during the downtime window. + +The biggest hurdle to achieving the above is Kibana’s Saved Object migrations. +Migrations aren’t resilient and require manual intervention anytime an error +occurs (see [3. Saved Object Migration +Errors](#3-saved-object-migration-errors)). + +It is impossible to discover these failures before initiating downtime. Errors +often force users to roll-back to a previous version of Kibana or cause hours +of downtime. To retry the migration, users are asked to manually delete a +`.kibana_x` index. If done incorrectly this can lead to data loss, making it a +terrifying experience (restoring from a pre-upgrade snapshot is a safer +alternative but not mentioned in the docs or logs). + +Cloud users don’t have access to Kibana logs to be able to identify and remedy +the cause of the migration failure. Apart from blindly retrying migrations by +restoring a previous snapshot, cloud users are unable to remedy a failed +migration and have to escalate to support which can further delay resolution. + +Taken together, version upgrades are a major operational risk and discourage +users from adopting the latest features. + +# 3. Saved Object Migration Errors + +Any of the following classes of errors could result in a Saved Object +migration failure which requires manual intervention to resolve: + +1. A bug in a plugin’s registered document transformation function causes it + to throw an exception on _valid_ data. +2. _Invalid_ data stored in Elasticsearch causes a plugin’s registered + document transformation function to throw an exception . +3. Failures resulting from an unhealthy Elasticsearch cluster: + 1. Maximum shards open + 2. Too many scroll contexts + 3. `circuit_breaking_exception` (insufficient heap memory) + 4. `process_cluster_event_timeout_exception` for index-aliases, create-index, put-mappings + 5. Read-only indices due to low disk space (hitting the flood_stage watermark) + 6. Re-index failed: search rejected due to missing shards + 7. `TooManyRequests` while doing a `count` of documents requiring a migration + 8. Bulk write failed: primary shard is not active +4. The Kibana process is killed while migrations are in progress. + +# 4. Design +## 4.0 Assumptions and tradeoffs +The proposed design makes several important assumptions and tradeoffs. + +**Background:** + +The 7.x upgrade documentation lists taking an Elasticsearch snapshot as a +required step, but we instruct users to retry migrations and perform rollbacks +by deleting the failed `.kibana_n` index and pointing the `.kibana` alias to +`.kibana_n-1`: + - [Handling errors during saved object +migrations.](https://github.com/elastic/kibana/blob/75444a9f1879c5702f9f2b8ad4a70a3a0e75871d/docs/setup/upgrade/upgrade-migrations.asciidoc#handling-errors-during-saved-object-migrations) + - [Rolling back to a previous version of Kibana.](https://github.com/elastic/kibana/blob/75444a9f1879c5702f9f2b8ad4a70a3a0e75871d/docs/setup/upgrade/upgrade-migrations.asciidoc#rolling-back-to-a-previous-version-of-kib) + - Server logs from failed migrations. + +**Assumptions and tradeoffs:** +1. It is critical to maintain a backup index during 7.x to ensure that anyone + following the existing upgrade / rollback procedures don't end up in a + position where they no longer can recover their data. + 1. This excludes us from introducing in-place migrations to support huge + indices during 7.x. +2. The simplicity of idempotent, coordination-free migrations outweighs the + restrictions this will impose on the kinds of migrations we're able to + support in the future. See (4.2.1) +3. A saved object type (and it's associated migrations) will only ever be + owned by one plugin. If pluginA registers saved object type `plugin_a_type` + then pluginB must never register that same type, even if pluginA is + disabled. Although we cannot enforce it on third-party plugins, breaking + this assumption may lead to data loss. + +## 4.1 Discover and remedy potential failures before any downtime + +> Achieves goals: (2.3) +> Mitigates errors: (3.1), (3.2) + +1. Introduce a CLI option to perform a dry run migration to allow + administrators to locate and fix potential migration failures without + taking their existing Kibana node(s) offline. +2. To have the highest chance of surfacing potential failures such as low disk + space, dry run migrations should not be mere simulations. A dry run should + perform a real migration in a way that doesn’t impact the existing Kibana + cluster. +3. The CLI should generate a migration report to make it easy to create a + support request from a failed migration dry run. + 1. The report would be an NDJSON export of all failed objects. + 2. If support receives such a report, we could modify all the objects to + ensure the migration would pass and send this back to the client. + 3. The client can then import the updated objects using the standard Saved + Objects NDJSON import and run another dry run to verify all problems + have been fixed. +4. Make running dry run migrations a required step in the upgrade procedure + documentation. +5. (Optional) Add dry run migrations to the standard cloud upgrade procedure? + +## 4.2 Automatically retry failed migrations until they succeed + +> Achieves goals: (2.2), (2.6) +> Mitigates errors (3.3) and (3.4) + +External conditions such as failures from an unhealthy Elasticsearch cluster +(3.3) can cause the migration to fail. The Kibana cluster should be able to +recover automatically once these external conditions are resolved. There are +two broad approaches to solving this problem based on whether or not +migrations are idempotent: + +| Idempotent migrations |Description | +| --------------------- | --------------------------------------------------------- | +| Yes | Idempotent migrations performed without coordination | +| No | Single node migrations coordinated through a lease / lock | + +Idempotent migrations don't require coordination making the algorithm +significantly less complex and will never require manual intervention to +retry. We, therefore, prefer this solution, even though it introduces +restrictions on migrations (4.2.1.1). For other alternatives that were +considered see section [(5)](#5-alternatives). + +## 4.2.1 Idempotent migrations performed without coordination + +The migration system can be said to be idempotent if the same results are +produced whether the migration was run once or multiple times. This property +should hold even if new (up to date) writes occur in between migration runs +which introduces the following restrictions: + +### 4.2.1.1 Restrictions + +1. All document transforms need to be deterministic, that is a document + transform will always return the same result for the same set of inputs. +2. It should always be possible to construct the exact set of inputs required + for (1) at any point during the migration process (before, during, after). + +Although these restrictions require significant changes, it does not prevent +known upcoming migrations such as [sharing saved-objects in multiple spaces](https://github.com/elastic/kibana/issues/27004) or [splitting a saved +object into multiple child +documents](https://github.com/elastic/kibana/issues/26602). To ensure that +these migrations are idempotent, they will have to generate new saved object +id's deterministically with e.g. UUIDv5. + + +### 4.2.1.2 Migration algorithm: Cloned index per version +Note: +- The description below assumes the migration algorithm is released in 7.10.0. + So < 7.10.0 will use `.kibana` and >= 7.10.0 will use `.kibana_current`. +- We refer to the alias and index that outdated nodes use as the source alias + and source index. +- Every version performs a migration even if mappings or documents aren't outdated. + +1. Locate the source index by fetching aliases (including `.kibana` for + versions prior to v7.10.0) + + ``` + GET '/_alias/.kibana_current,.kibana_7.10.0,.kibana' + ``` + + The source index is: + 1. the index the `.kibana_current` alias points to, or if it doesn’t exist, + 2. the index the `.kibana` alias points to, or if it doesn't exist, + 3. the v6.x `.kibana` index + + If none of the aliases exists, this is a new Elasticsearch cluster and no + migrations are necessary. Create the `.kibana_7.10.0_001` index with the + following aliases: `.kibana_current` and `.kibana_7.10.0`. +2. If `.kibana_current` and `.kibana_7.10.0` both exists and are pointing to the same index this version's migration has already been completed. + 1. Because the same version can have plugins enabled at any point in time, + perform the mappings update in step (6) and migrate outdated documents + with step (7). + 2. Skip to step (9) to start serving traffic. +3. Fail the migration if: + 1. `.kibana_current` is pointing to an index that belongs to a later version of Kibana .e.g. `.kibana_7.12.0_001` + 2. (Only in 8.x) The source index contains documents that belong to an unknown Saved Object type (from a disabled plugin). Log an error explaining that the plugin that created these documents needs to be enabled again or that these objects should be deleted. See section (4.2.1.4). +4. Mark the source index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. +5. Clone the source index into a new target index which has writes enabled. All nodes on the same version will use the same fixed index name e.g. `.kibana_7.10.0_001`. The `001` postfix isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`. + 1. `POST /.kibana_n/_clone/.kibana_7.10.0_001?wait_for_active_shards=all {"settings": {"index.blocks.write": false}}`. Ignore errors if the clone already exists. + 2. Wait for the cloning to complete `GET /_cluster/health/.kibana_7.10.0_001?wait_for_status=green&timeout=60s` If cloning doesn’t complete within the 60s timeout, log a warning for visibility and poll again. +6. Update the mappings of the target index + 1. Retrieve the existing mappings including the `migrationMappingPropertyHashes` metadata. + 2. Update the mappings with `PUT /.kibana_7.10.0_001/_mapping`. The API deeply merges any updates so this won't remove the mappings of any plugins that were enabled in a previous version but are now disabled. + 3. Ensure that fields are correctly indexed using the target index's latest mappings `POST /.kibana_7.10.0_001/_update_by_query?conflicts=proceed`. In the future we could optimize this query by only targeting documents: + 1. That belong to a known saved object type. + 2. Which don't have outdated migrationVersion numbers since these will be transformed anyway. + 3. That belong to a type whose mappings were changed by comparing the `migrationMappingPropertyHashes`. (Metadata, unlike the mappings isn't commutative, so there is a small chance that the metadata hashes do not accurately reflect the latest mappings, however, this will just result in an less efficient query). +7. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. + 1. Ignore any version conflict errors. + 2. If a document transform throws an exception, add the document to a failure list and continue trying to transform all other documents. If any failures occured, log the complete list of documents that failed to transform. Fail the migration. +8. Mark the migration as complete by doing a single atomic operation (requires https://github.com/elastic/elasticsearch/pull/58100) that: + 1. Checks that `.kibana-current` alias is still pointing to the source index + 2. Points the `.kibana-7.10.0` and `.kibana_current` aliases to the target index. + 3. If this fails with a "required alias [.kibana_current] does not exist" error fetch `.kibana_current` again: + 1. If `.kibana_current` is _not_ pointing to our target index fail the migration. + 2. If `.kibana_current` is pointing to our target index the migration has succeeded and we can proceed to step (9). +9. Start serving traffic. + +Together with the limitations, this algorithm ensures that migrations are +idempotent. If two nodes are started simultaneously, both of them will start +transforming documents in that version's target index, but because migrations are idempotent, it doesn’t matter which node’s writes win. + +
+ In the future, this algorithm could enable (2.6) "read-only functionality during the downtime window" but this is outside of the scope of this RFC. + + Although the migration algorithm guarantees there's no data loss while providing read-only access to outdated nodes, this could cause plugins to behave in unexpected ways. If we wish to persue it in the future, enabling read-only functionality during the downtime window will be it's own project and must include an audit of all plugins' behaviours. +
+ +### 4.2.1.3 Upgrade and rollback procedure +When a newer Kibana starts an upgrade, it blocks all writes to the outdated index to prevent data loss. Since Kibana is not designed to gracefully handle a read-only index this could have unintended consequences such as a task executing multiple times but never being able to write that the task was completed successfully. To prevent unintended consequences, the following procedure should be followed when upgrading Kibana: + +1. Gracefully shutdown outdated nodes by sending a `SIGTERM` signal + 1. Node starts returning `503` from it's healthcheck endpoint to signal to + the load balancer that it's no longer accepting new traffic (requires https://github.com/elastic/kibana/issues/46984). + 2. Allows ungoing HTTP requests to complete with a configurable timeout + before forcefully terminating any open connections. + 3. Closes any keep-alive sockets by sending a `connection: close` header. + 4. Shutdown all plugins and Core services. +2. (recommended) Take a snapshot of all Kibana's Saved Objects indices. This simplifies doing a rollback to a simple snapshot restore, but is not required in order to do a rollback if a migration fails. +3. Start the upgraded Kibana nodes. All running Kibana nodes should be on the same version, have the same plugins enabled and use the same configuration. + +To rollback to a previous version of Kibana with a snapshot +1. Shutdown all Kibana nodes. +2. Restore the Saved Object indices and aliases from the snapshot +3. Start the rollback Kibana nodes. All running Kibana nodes should be on the same rollback version, have the same plugins enabled and use the same configuration. + +To rollback to a previous version of Kibana without a snapshot: +(Assumes the migration to 7.11.0 failed) +1. Shutdown all Kibana nodes. +2. Remove the index created by the failed Kibana migration by using the version-specific alias e.g. `DELETE /.kibana_7.11.0` +3. Identify the rollback index: + 1. If rolling back to a Kibana version < 7.10.0 use `.kibana` + 2. If rolling back to a Kibana version >= 7.10.0 use the version alias of the Kibana version you wish to rollback to e.g. `.kibana_7.10.0` +4. Point the `.kibana_current` alias to the rollback index. +5. Remove the write block from the rollback index. +6. Start the rollback Kibana nodes. All running Kibana nodes should be on the same rollback version, have the same plugins enabled and use the same configuration. + +### 4.2.1.4 Handling documents that belong to a disabled plugin +It is possible for a plugin to create documents in one version of Kibana, but then when upgrading Kibana to a newer version, that plugin is disabled. Because the plugin is disabled it cannot register it's Saved Objects type including the mappings or any migration transformation functions. These "orphan" documents could cause future problems: + - A major version introduces breaking mapping changes that cannot be applied to the data in these documents. + - Two majors later migrations will no longer be able to migrate this old schema and could fail unexpectadly when the plugin is suddenly enabled. + +As a concrete example of the above, consider a user taking the following steps: +1. Installs Kibana 7.6.0 with spaces=enabled. The spaces plugin creates a default space saved object. +2. User upgrades to 7.10.0 but uses the OSS download which has spaces=disabled. Although the 7.10.0 spaces plugin includes a migration for space documents, the OSS release cannot migrate the documents or update it's mappings. +3. User realizes they made a mistake and use Kibana 7.10.0 with x-pack and the spaces plugin enabled. At this point we have a completed migration for 7.10.0 but there's outdated spaces documents with migrationVersion=7.6.0 instead of 7.10.0. + +There are several approaches we could take to dealing with these orphan documents: + +1. Start up but refuse to query on types with outdated documents until a user manually triggers a re-migration + + Advantages: + - The impact is limited to a single plugin + + Disadvantages: + - It might be less obvious that a plugin is in a degraded state unless you read the logs (not possible on Cloud) or view the `/status` endpoint. + - If a user doesn't care that the plugin is degraded, orphan documents are carried forward indefinitely. + - Since Kibana has started receiving traffic, users can no longer + downgrade without losing data. They have to re-migrate, but if that + fails they're stuck. + - Introduces a breaking change in the upgrade behaviour + + To perform a re-migration: + - Remove the `.kibana_7.10.0` alias + - Take a snapshot OR set the configuration option `migrations.target_index_postfix: '002'` to create a new target index `.kibana_7.10.0_002` and keep the `.kibana_7.10.0_001` index to be able to perform a rollback. + - Start up Kibana + +2. Refuse to start Kibana until the plugin is enabled or it's data deleted + + Advantages: + - Admin’s are forced to deal with the problem as soon as they disable a plugin + + Disadvantages: + - Cannot temporarily disable a plugin to aid in debugging or to reduce the load a Kibana plugin places on an ES cluster. + - Introduces a breaking change + +3. Refuse to start a migration until the plugin is enabled or it's data deleted + + Advantages: + - We force users to enable a plugin or delete the documents which prevents these documents from creating future problems like a mapping update not being compatible because there are fields which are assumed to have been migrated. + - We keep the index “clean”. + + Disadvantages: + - Since users have to take down outdated nodes before they can start the upgrade, they have to enter the downtime window before they know about this problem. This prolongs the downtime window and in many cases might cause an operations team to have to reschedule their downtime window to give them time to investigate the documents that need to be deleted. Logging an error on every startup could warn users ahead of time to mitigate this. + - We don’t expose Kibana logs on Cloud so this will have to be escalated to support and could take 48hrs to resolve (users can safely rollback, but without visibility into the logs they might not know this). Exposing Kibana logs is on the cloud team’s roadmap. + - It might not be obvious just from the saved object type, which plugin created these objects. + - Introduces a breaking change in the upgrade behaviour + +4. Use a hash of enabled plugins as part of the target index name + Using a migration target index name like + `.kibana_7.10.0_${hash(enabled_plugins)}_001` we can migrate all documents + every time a plugin is enabled / disabled. + + Advantages: + - Outdated documents belonging to disabled plugins will be upgraded as soon + as the plugin is enabled again. + + Disadvantages: + - Disabling / enabling a plugin will cause downtime (breaking change). + - When a plugin is enabled, disabled and enabled again our target index + will be an existing outdated index which needs to be deleted and + re-cloned. Without a way to check if the index is outdated, we cannot + deterministically perform the delete and re-clone operation without + coordination. + +5. Transform outdated documents (step 7) on every startup + Advantages: + - Outdated documents belonging to disabled plugins will be upgraded as soon + as the plugin is enabled again. + + Disadvantages: + - Orphan documents are retained indefinitely so there's still a potential + for future problems. + - Slightly slower startup time since we have to query for outdated + documents every time. + +We prefer option (3) since it provides flexibility for disabling plugins in +the same version while also protecting users' data in all cases during an +upgrade migration. However, because this is a breaking change we will +implement (5) during 7.x and only implement (3) during 8.x. + +# 5. Alternatives +## 5.1 Rolling upgrades +We considered implementing rolling upgrades to provide zero downtime +migrations. However, this would introduce significant complexity for plugins: +they will need to maintain up and down migration transformations and ensure +that queries match both current and outdated documents across all +versions. Although we can afford the once-off complexity of implementing +rolling upgrades, the complexity burden of maintaining plugins that support +rolling-upgrades will slow down all development in Kibana. Since a predictable +downtime window is sufficient for our users, we decided against trying to +achieve zero downtime with rolling upgrades. See "Rolling upgrades" in +https://github.com/elastic/kibana/issues/52202 for more information. + +## 5.2 Single node migrations coordinated through a lease/lock +This alternative is a proposed algorithm for coordinating migrations so that +these only happen on a single node and therefore don't have the restrictions +found in [(4.2.1.1)](#4311-restrictions). We decided against this algorithm +primarily because it is a lot more complex, but also because it could still +require manual intervention to retry from certain unlikely edge cases. + +
+ It's impossible to guarantee that a single node performs the + migration and automatically retry failed migrations. + +Coordination should ensure that only one Kibana node performs the migration at +a given time which can be achived with a distributed lock built on top of +Elasticsearch. For the Kibana cluster to be able to retry a failed migration, +requires a specialized lock which expires after a given amount of inactivity. +We will refer to such expiring locks as a "lease". + +If a Kibana process stalls, it is possible that the process' lease has expired +but the process doesn't yet recognize this and continues the migration. To +prevent this from causing data loss each lease should be accompanied by a +"guard" that prevents all writes after the lease has expired. See +[how to do distributed +locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) +for an in-depth discussion. + +Elasticsearch doesn't provide any building blocks for constructing such a guard. +
+ +However, we can implement a lock (that never expires) with strong +data-consistency guarantees. Because there’s no expiration, a failure between +obtaining the lock and releasing it will require manual intervention. Instead +of trying to accomplish the entire migration after obtaining a lock, we can +only perform the last step of the migration process, moving the aliases, with +a lock. A permanent failure in only this last step is not impossible, but very +unlikely. + +### 5.2.1 Migration algorithm +1. Obtain a document lock (see [5.2.2 Document lock + algorithm](#522-document-lock-algorithm)). Convert the lock into a "weak + lease" by expiring locks for nodes which aren't active (see [4.2.2.4 + Checking for lease expiry](#4324-checking-for-lease-expiry)). This "weak + lease" doesn't require strict guarantees since it's only used to prevent + multiple Kibana nodes from performing a migration in parallel to reduce the + load on Elasticsearch. +2. Migrate data into a new process specific index (we could use the process + UUID that’s used in the lease document like + `.kibana_3ef25ff1-090a-4335-83a0-307a47712b4e`). +3. Obtain a document lock (see [5.2.2 Document lock + algorithm](#522-document-lock-algorithm)). +4. Finish the migration by pointing `.kibana` → + `.kibana_3ef25ff1-090a-4335-83a0-307a47712b4e`. This automatically releases + the document lock (and any leases) because the new index will contain an + empty `kibana_cluster_state`. + +If a process crashes or is stopped after (3) but before (4) the lock will have +to be manually removed by deleting the `kibana_cluster_state` document from +`.kibana` or restoring from a snapshot. + +### 5.2.2 Document lock algorithm +To improve on the existing Saved Objects migrations lock, a locking algorithm +needs to satisfy the following requirements: +- Must guarantee that only a single node can obtain the lock. Since we can + only provide strong data-consistency guarantees on the document level in + Elasticsearch our locking mechanism needs to be based on a document. +- Manually removing the lock + - shouldn't have any risk of accidentally causing data loss. + - can be done with a single command that's always the same (shouldn’t + require trying to find `n` for removing the correct `.kibana_n` index). +- Must be easy to retrieve the lock/cluster state to aid in debugging or to + provide visibility. + +Algorithm: +1. Node reads `kibana_cluster_state` lease document from `.kibana` +2. It sends a heartbeat every `heartbeat_interval` seconds by sending an + update operation that adds it’s UUID to the `nodes` array and sets the + `lastSeen` value to the current local node time. If the update fails due to + a version conflict the update operation is retried after a random delay by + fetching the document again and attempting the update operation once more. +3. To obtain a lease, a node: + 1. Fetches the `kibana_cluster_state` document + 2. If all the nodes’ `hasLock === false` it sets it’s own `hasLock` to + true and attempts to write the document. If the update fails + (presumably because of another node’s heartbeat update) it restarts the + process to obtain a lease from step (3). + 3. If another nodes’ `hasLock === true` the node failed to acquire a + lock and waits until the active lock has expired before attempting to + obtain a lock again. +4. Once a node is done with its lock, it releases it by fetching and then + updating `hasLock = false`. The fetch + update operations are retried until + this node’s `hasLock === false`. + +Each machine writes a `UUID` to a file, so a single machine may have multiple +processes with the same Kibana `UUID`, so we should rather generate a new UUID +just for the lifetime of this process. + +`KibanaClusterState` document format: +```js + nodes: { + "852bd94e-5121-47f3-a321-e09d9db8d16e": { + version: "7.6.0", + lastSeen: [ 1114793, 555149266 ], // hrtime() big int timestamp + hasLease: true, + hasLock: false, + }, + "8d975c5b-cbf6-4418-9afb-7aa3ea34ac90": { + version: "7.6.0", + lastSeen: [ 1114862, 841295591 ], + hasLease: false, + hasLock: false, + }, + "3ef25ff1-090a-4335-83a0-307a47712b4e": { + version: "7.6.0", + lastSeen: [ 1114877, 611368546 ], + hasLease: false, + hasLock: false, + }, + }, + oplog: [ + {op: 'ACQUIRE_LOCK', node: '852bd94e...', timestamp: '2020-04-20T11:58:56.176Z'} + ] +} +``` + +### 5.2.3 Checking for "weak lease" expiry +The simplest way to check for lease expiry is to inspect the `lastSeen` value. +If `lastSeen + expiry_timeout > now` the lock is considered expired. If there +are clock drift or daylight savings time adjustments, there’s a risk that a +node loses it’s lease before `expiry_timeout` has occurred. Since losing a +lock prematurely will not lead to data loss it’s not critical that the +expiry time is observed under all conditions. + +A slightly safer approach is to use a monotonically increasing clock +(`process.hrtime()`) and relative time to determine expiry. Using a +monotonically increasing clock guarantees that the clock will always increase +even if the system time changes due to daylight savings time, NTP clock syncs, +or manually setting the time. To check for expiry, other nodes poll the +cluster state document. Once they see that the `lastSeen` value has increased, +they capture the current hr time `current_hr_time` and starts waiting until +`process.hrtime() - current_hr_time > expiry_timeout` if at that point +`lastSeen` hasn’t been updated the lease is considered to have expired. This +means other nodes can take up to `2*expiry_timeout` to recognize an expired +lease, but a lease will never expire prematurely. + +Any node that detects an expired lease can release that lease by setting the +expired node’s `hasLease = false`. It can then attempt to acquire its lease. + +## 5.3 Minimize data loss with mixed Kibana versions during 7.x +When multiple versions of Kibana are running at the same time, writes from the +outdated node can end up either in the outdated Kibana index, the newly +migrated index, or both. New documents added (and some updates) into the old +index while a migration is in-progress will be lost. Writes that end up in the +new index will be in an outdated format. This could cause queries on the data +to only return a subset of the results which leads to incorrect results or +silent data loss. + +Minimizing data loss from mixed 7.x versions, introduces two additional steps +to rollback to a previous version without a snapshot: +1. (existing) Point the `.kibana` alias to the previous Kibana index `.kibana_n-1` +2. (existing) Delete `.kibana_n` +3. (new) Enable writes on `.kibana_n-1` +4. (new) Delete the dummy "version lock" document from `.kibana_n-1` + +Since our documentation and server logs have implicitly encouraged users to +rollback without using snapshots, many users might have to rely on these +additional migration steps to perform a rollback. Since even the existing +steps are error prone, introducing more steps will likely introduce more +problems than what it solves. + +1. All future versions of Kibana 7.x will use the `.kibana_saved_objects` + alias to locate the current index. If `.kibana_saved_objects` doesn't + exist, newer versions will fallback to reading `.kibana`. +2. All future versions of Kibana will locate the index that + `.kibana_saved_objects` points to and then read and write directly from + the _index_ instead of the alias. +3. Before starting a migration: + 1. Write a new dummy "version lock" document to the `.kibana` index with a + `migrationVersion` set to the current version of Kibana. If an outdated + node is started up after a migration was started it will detect that + newer documents are present in the index and refuse to start up. + 2. Set the outdated index to read-only. Since `.kibana` is never advanced, + it will be pointing to a read-only index which prevent writes from + 6.8+ releases which are already online. + +## 5.4 In-place migrations that re-use the same index (8.0) +> We considered an algorithm that re-uses the same index for migrations and an approach to minimize data-loss if our upgrade procedures aren't followed. This is no longer our preferred approach because of several downsides: +> - It requires taking snapshots to prevent data loss so we can only release this in 8.x +> - Minimizing data loss with unsupported upgrade configurations adds significant complexity and still doesn't guarantee that data isn't lost. + +### 5.4.1 Migration algorithm (8.0): +1. Exit Kibana with a fatal error if a newer node has started a migration by + checking for: + 1. Documents with a newer `migrationVersion` numbers. +2. If the mappings are out of date, update the mappings to the combination of + the index's current mappings and the expected mappings. +3. If there are outdated documents, migrate these in batches: + 1. Read a batch of outdated documents from the index. + 2. Transform documents by applying the migration transformation functions. + 3. Update the document batch in the same index using optimistic concurrency + control. If a batch fails due to an update version mismatch continue + migrating the other batches. + 4. If a batch fails due other reasons repeat the entire migration process. +4. If any of the batches in step (3.3) failed, repeat the entire migration + process. This ensures that in-progress bulk update operations from an + outdated node won't lead to unmigrated documents still being present after + the migration. +5. Once all documents are up to date, the migration is complete and Kibana can + start serving traffic. + +Advantages: +- Not duplicating all documents into a new index will speed up migrations and + reduce the downtime window. This will be especially important for the future + requirement to support > 10k or > 100k documents. +- We can check the health of an existing index before starting the migration, + but we cannot detect what kind of failures might occur while creating a new + index. Whereas retrying migrations will eventually recover from the errors + in (3.3), re-using an index allows us to detect these problems before trying + and avoid errors like (3.3.1) altogether. +- Single index to backup instead of “index pattern” that matches any + `.kibana_n`. +- Simplifies Kibana system index Elasticsearch plugin since it needs to work + on one index per "tenant". +- By leveraging optimistic concurrency control we can further minimize data + loss for unsupported upgrade configurations in the future. + +Drawbacks: +- Cannot make breaking mapping changes (even though it was possible, we have not + introduced a breaking mapping change during 7.x). +- Rollback is only possible by restoring a snapshot which requires educating + users to ensure that they don't rely on `.kibana_n` indices as backups. + (Apart from the need to educate users, snapshot restores provide many + benefits). +- It narrows the second restriction under (4.2.1) even further: migrations + cannot rely on any state that could change as part of a migration because we + can no longer use the previous index as a snapshot of unmigrated state. +- We can’t automatically perform a rollback from a half-way done migration. +- It’s impossible to provide read-only functionality for outdated nodes which + means we can't achieve goal (2.7). + +### 5.4.2 Minimizing data loss with unsupported upgrade configurations (8.0) +> This alternative can reduce some data loss when our upgrade procedure isn't +> followed with the algorithm in (5.4.1). + +Even if (4.5.2) is the only supported upgrade procedure, we should try to +prevent data loss when these instructions aren't followed. + +To prevent data loss we need to prevent any writes from older nodes. We use +a version-specific alias for this purpose. Each time a migration is started, +all other aliases are removed. However, aliases are stored inside +Elasticsearch's ClusterState and this state could remain inconsistent between +nodes for an unbounded amount of time. In addition, bulk operations that were +accepted before the alias was removed will continue to run even after removing +the alias. + +As a result, Kibana cannot guarantee that there would be no data loss but +instead, aims to minimize it as much as possible by adding the bold sections +to the migration algorithm from (5.4.1) + +1. **Disable `action.auto_create_index` for the Kibana system indices.** +2. Exit Kibana with a fatal error if a newer node has started a migration by + checking for: + 1. **Version-specific aliases on the `.kibana` index with a newer version.** + 2. Documents with newer `migrationVersion` numbers. +3. **Remove all other aliases and create a new version-specific alias for + reading and writing to the `.kibana` index .e.g `.kibana_8.0.1`. During and + after the migration, all saved object reads and writes use this alias + instead of reading or writing directly to the index. By using the atomic + `POST /_aliases` API we minimize the chance that an outdated node creating + new outdated documents can cause data loss.** +4. **Wait for the default bulk operation timeout of 30s. This ensures that any + bulk operations accepted before the removal of the alias have either + completed or returned a timeout error to it's initiator.** +5. If the mappings are out of date, update the mappings **through the alias** + to the combination of the index's current mappings and the expected + mappings. **If this operation fails due to an index missing exception (most + likely because another node removed our version-specific alias) repeat the + entire migration process.** +6. If there are outdated documents, migrate these in batches: + 1. Read a batch of outdated documents from `.kibana_n`. + 2. Transform documents by applying the migration functions. + 3. Update the document batch in the same index using optimistic concurrency + control. If a batch fails due to an update version mismatch continue + migrating the other batches. + 4. If a batch fails due other reasons repeat the entire migration process. +7. If any of the batches in step (6.3) failed, repeat the entire migration + process. This ensures that in-progress bulk update operations from an + outdated node won't lead to unmigrated documents still being present after + the migration. +8. Once all documents are up to date, the migration is complete and Kibana can + start serving traffic. + +Steps (2) and (3) from the migration algorithm in minimize the chances of the +following scenarios occuring but cannot guarantee it. It is therefore useful +to enumarate some scenarios and their worst case impact: +1. An outdated node issued a bulk create to it's version-specific alias. + Because a user doesn't wait for all traffic to drain a newer node starts + it's migration before the bulk create was complete. Since this bulk create + was accepted before the newer node deleted the previous version-specific + aliases, it is possible that the index now contains some outdated documents + that the new node is unaware of and doesn't migrate. Although these outdated + documents can lead to inconsistent query results and data loss, step (4) + ensures that an error will be returned to the node that created these + objects. +2. A 8.1.0 node and a 8.2.0 node starts migrating a 8.0.0 index in parallel. + Even though the 8.2.0 node will remove the 8.1.0 version-specific aliases, + the 8.1.0 node could have sent an bulk update operation that got accepted + before its alias was removed. When the 8.2.0 node tries to migrate these + 8.1.0 documents it gets a version conflict but cannot be sure if this was + because another node of the same version migrated this document (which can + safely be ignored) or interference from a different Kibana version. The + 8.1.0 node will hit the error in step (6.3) and restart the migration but + then ultimately fail at step (2). The 8.2.0 node will repeat the entire + migration process from step (7) thus ensuring that all documents are up to + date. +3. A race condition with another Kibana node on the same version, but with + different enabled plugins caused this node's required mappings to be + overwritten. If this causes a mapper parsing exception in step (6.3) we can + restart the migration. Because updating the mappings is additive and saved + object types are unique to a plugin, restarting the migration will allow + the node to update the mappings to be compatible with node's plugins. Both + nodes will be able to successfully complete the migration of their plugins' + registered saved object types. However, if the migration doesn't trigger a + mapper parsing exception the incompatible mappings would go undetected + which can cause future problems like write failures or inconsistent query + results. + +## 5.5 Tag objects as “invalid” if their transformation fails +> This alternative prevents a failed migration when there's a migration transform function bug or a document with invalid data. Although it seems preferable to not fail the entire migration because of a single saved object type's migration transform bug or a single invalid document this has several pitfalls: +> 1. When an object fails to migrate the data for that saved object type becomes inconsistent. This could load to a critical feature being unavailable to a user leaving them with no choice but to downgrade. +> 2. Because Kibana starts accepting traffic after encountering invalid objects a rollback will lead to data loss leaving users with no clean way to recover. +> As a result we prefer to let an upgrade fail and making it easy for users to rollback until they can resolve the root cause. + +> Achieves goals: (2.2) +> Mitigates Errors (3.1), (3.2) + +1. Tag objects as “invalid” if they cause an exception when being transformed, + but don’t fail the entire migration. +2. Log an error message informing administrators that there are invalid + objects which require inspection. For each invalid object, provide an error + stack trace to aid in debugging. +3. Administrators should be able to generate a migration report (similar to + the one dry run migrations create) which is an NDJSON export of all objects + tagged as “invalid”. + 1. Expose this as an HTTP API first + 2. (later) Notify administrators and allow them to export invalid objects + from the Kibana UI. +4. When an invalid object is read, the Saved Objects repository will throw an + invalid object exception which should include a link to the documentation + to help administrators resolve migration bugs. +5. Educate Kibana developers to no longer simply write back an unmigrated + document if an exception occurred. A migration function should either + successfully transform the object or throw. + +# 6. How we teach this +1. Update documentation and server logs to start educating users to depend on + snapshots for Kibana rollbacks. +2. Update developer documentation and educate developers with best practices + for writing migration functions. + +# 7. Unresolved questions +1. When cloning an index we can only ever add new fields to the mappings. When + a saved object type or specific field is removed, the mappings will remain + until we re-index. Is it sufficient to only re-index every major? How do we + track the field count as it grows over every upgrade? +2. More generally, how do we deal with the growing field count approaching the + default limit of 1000? diff --git a/scripts/es.js b/scripts/es.js index 93f1d69350bac2..2d56496f2fdd23 100644 --- a/scripts/es.js +++ b/scripts/es.js @@ -17,7 +17,7 @@ * under the License. */ -require('../src/setup_node_env'); +require('../src/setup_node_env/prebuilt_dev_only_entry'); var resolve = require('path').resolve; var pkg = require('../package.json'); diff --git a/scripts/load_team_assignment.js b/scripts/generate_team_assignments.js similarity index 96% rename from scripts/load_team_assignment.js rename to scripts/generate_team_assignments.js index b8f5edc8336342..9dcb9bb90e0fd6 100644 --- a/scripts/load_team_assignment.js +++ b/scripts/generate_team_assignments.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/code_coverage/ingest_coverage/team_assignment').uploadTeamAssignmentJson(); +require('../src/dev/code_coverage/ingest_coverage/team_assignment').generateTeamAssignments(); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index e3f992990f9f95..d9821364222686 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -107,7 +107,7 @@ describe('AppRouter', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -119,7 +119,7 @@ describe('AppRouter', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -133,7 +133,7 @@ describe('AppRouter', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -145,7 +145,7 @@ describe('AppRouter', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -157,7 +157,7 @@ describe('AppRouter', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -171,7 +171,7 @@ describe('AppRouter', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -183,7 +183,7 @@ describe('AppRouter', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -195,7 +195,7 @@ describe('AppRouter', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 089d1cf3f3ced8..9821db5ba26661 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -25,7 +25,7 @@ import React, { useState, MutableRefObject, } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingElastic } from '@elastic/eui'; import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; @@ -120,7 +120,7 @@ export const AppContainer: FunctionComponent = ({ {appNotFound && } {showSpinner && (
- +
)}
diff --git a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap index 3007be1e5dfe0e..e6bf7e898d8c48 100644 --- a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap +++ b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap @@ -1,23 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kbnLoadingIndicator is hidden by default 1`] = ` -
-
-
+/> `; exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = ` -
-
-
+/> `; diff --git a/src/core/public/chrome/ui/_loading_indicator.scss b/src/core/public/chrome/ui/_loading_indicator.scss index ad934717b4b766..ccf1ecc873fc5c 100644 --- a/src/core/public/chrome/ui/_loading_indicator.scss +++ b/src/core/public/chrome/ui/_loading_indicator.scss @@ -1,55 +1,4 @@ -$kbnLoadingIndicatorBackgroundSize: $euiSizeXXL * 10; -$kbnLoadingIndicatorColor1: tint($euiColorAccent, 15%); -$kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%); - -/** - * 1. Position this loader on top of the content. - * 2. Make sure indicator isn't wider than the screen. - */ -.kbnLoadingIndicator { - position: fixed; // 1 - top: 0; // 1 - left: 0; // 1 - right: 0; // 1 - z-index: $euiZLevel2; // 1 - overflow: hidden; // 2 - height: $euiSizeXS / 2; - - &.hidden { - visibility: hidden; - opacity: 0; - transition-delay: 0.25s; - } -} - -.kbnLoadingIndicator__bar { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - z-index: $euiZLevel2 + 1; - visibility: visible; - display: block; - animation: kbn-animate-loading-indicator 2s linear infinite; - background-color: $kbnLoadingIndicatorColor2; - background-image: linear-gradient( - to right, - $kbnLoadingIndicatorColor1 0%, - $kbnLoadingIndicatorColor1 50%, - $kbnLoadingIndicatorColor2 50%, - $kbnLoadingIndicatorColor2 100% - ); - background-repeat: repeat-x; - background-size: $kbnLoadingIndicatorBackgroundSize $kbnLoadingIndicatorBackgroundSize; - width: 200%; -} - -@keyframes kbn-animate-loading-indicator { - from { - transform: translateX(0); - } - to { - transform: translateX(-$kbnLoadingIndicatorBackgroundSize); - } +.kbnLoadingIndicator-hidden { + visibility: hidden; + animation-play-state: paused; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 1a4d93aee9516d..8937ecb4d9b4e3 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1690,66 +1690,6 @@ exports[`Header renders 1`] = ` } } > - -
-
-
-
, + , ], }, Object { @@ -2971,6 +2963,81 @@ exports[`Header renders 1`] = `
+ +
+ + + + + +
+
; + return ; } const toggleCollapsibleNavRef = createRef(); @@ -97,7 +97,6 @@ export function Header({ return ( <> -
, + , ], borders: 'none', }, diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 174c46981db53a..52412f8990c7a9 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -20,7 +20,7 @@ import { EuiHeaderBreadcrumbs } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index dee93ecb1a804f..83e0c52ab3f3a2 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -20,7 +20,7 @@ import { EuiHeaderLogo } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; diff --git a/src/core/public/chrome/ui/header/header_nav_controls.tsx b/src/core/public/chrome/ui/header/header_nav_controls.tsx index 8d9d8097fd8e3f..69b0e3bd8afe3a 100644 --- a/src/core/public/chrome/ui/header/header_nav_controls.tsx +++ b/src/core/public/chrome/ui/header/header_nav_controls.tsx @@ -19,7 +19,7 @@ import { EuiHeaderSectionItem } from '@elastic/eui'; import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeNavControl } from '../../nav_controls'; import { HeaderExtension } from './header_extension'; diff --git a/src/core/public/chrome/ui/loading_indicator.tsx b/src/core/public/chrome/ui/loading_indicator.tsx index 0209612eae08c8..ca3e95f722ec50 100644 --- a/src/core/public/chrome/ui/loading_indicator.tsx +++ b/src/core/public/chrome/ui/loading_indicator.tsx @@ -17,6 +17,8 @@ * under the License. */ +import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import classNames from 'classnames'; import { Subscription } from 'rxjs'; @@ -25,9 +27,12 @@ import { HttpStart } from '../../http'; export interface LoadingIndicatorProps { loadingCount$: ReturnType; + showAsBar?: boolean; } export class LoadingIndicator extends React.Component { + public static defaultProps = { showAsBar: false }; + private loadingCountSubscription?: Subscription; state = { @@ -50,16 +55,35 @@ export class LoadingIndicator extends React.Component -
-
+ const ariaHidden = this.state.visible ? false : true; + + const ariaLabel = i18n.translate('core.ui.loadingIndicatorAriaLabel', { + defaultMessage: 'Loading content', + }); + + return !this.props.showAsBar ? ( + + ) : ( + ); } } diff --git a/src/core/public/overlays/banners/user_banner_service.tsx b/src/core/public/overlays/banners/user_banner_service.tsx index 643d95a1e3bb4c..2b93c3e4b6c215 100644 --- a/src/core/public/overlays/banners/user_banner_service.tsx +++ b/src/core/public/overlays/banners/user_banner_service.tsx @@ -19,12 +19,11 @@ import React, { Fragment } from 'react'; import ReactDOM from 'react-dom'; -import ReactMarkdown from 'react-markdown'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiButton } from '@elastic/eui'; +import { EuiCallOut, EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { I18nStart } from '../../i18n'; import { IUiSettingsClient } from '../../ui_settings'; @@ -36,6 +35,8 @@ interface StartDeps { uiSettings: IUiSettingsClient; } +const ReactMarkdownLazy = React.lazy(() => import('react-markdown')); + /** * Sets up the custom banner that can be specified in advanced settings. * @internal @@ -75,7 +76,15 @@ export class UserBannerService { } iconType="help" > - {content.trim()} + + +
+ } + > + + banners.remove(id!)}> { expect(sut.inspect()).to.be('Right(undefined)'); }); }); - describe(`'fromNullable`, () => { + describe(`fromNullable`, () => { it(`should continue processing if a truthy is calculated`, () => { attempt({ detail: 'x' }).fold( () => {}, @@ -64,4 +64,18 @@ describe(`either datatype functions`, () => { attempt(false).fold(expectNull, () => {}); }); }); + describe(`predicate fns`, () => { + it(`right.isRight() is true`, () => { + expect(Either.right('a').isRight()).to.be(true); + }); + it(`right.isLeft() is false`, () => { + expect(Either.right('a').isLeft()).to.be(false); + }); + it(`left.isLeft() is true`, () => { + expect(Either.left().isLeft()).to.be(true); + }); + it(`left.isRight() is true`, () => { + expect(Either.left().isRight()).to.be(false); + }); + }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js new file mode 100644 index 00000000000000..371695337ed56d --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -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 expect from '@kbn/expect'; +import { enumeratePatterns } from '../team_assignment/enumerate_patterns'; +import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; + +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + +describe(`enumeratePatterns`, () => { + it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.js to kibana-reporting`, () => { + const actual = enumeratePatterns(REPO_ROOT)(log)( + new Map([['x-pack/plugins/reporting', ['kibana-reporting']]]) + ); + + expect( + actual[0].includes( + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting' + ) + ).to.be(true); + }); + it(`should resolve src/plugins/charts/public/static/color_maps/color_maps.ts to kibana-app`, () => { + const actual = enumeratePatterns(REPO_ROOT)(log)( + new Map([['src/plugins/charts/public/static/color_maps', ['kibana-app']]]) + ); + + expect(actual[0][0]).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app' + ); + }); + it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => { + const short = 'x-pack/plugins/security_solution'; + const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); + + expect( + actual[0].includes( + `${short}/public/common/components/exceptions/builder/translations.ts kibana-security` + ) + ).to.be(true); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js new file mode 100644 index 00000000000000..f480135b45ac64 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumeration_helpers.test.js @@ -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 expect from '@kbn/expect'; +import { tryPath } from '../team_assignment/enumeration_helpers'; + +describe(`enumeration helper fns`, () => { + describe(`tryPath`, () => { + describe(`w/o glob file paths`, () => { + it(`should return a right on an existing path`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/ingest.js'; + const actual = tryPath(aPath); + expect(actual.isRight()).to.be(true); + }); + it(`should return a left on a non existing path`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/does_not_exist.js'; + const actual = tryPath(aPath); + expect(actual.isLeft()).to.be(true); + }); + }); + describe(`with glob file paths`, () => { + it(`should not error when the glob expands to nothing, but instead return a Left`, () => { + const aPath = 'src/legacy/core_plugins/kibana/public/home/*.ts'; + const actual = tryPath(aPath); + expect(actual.isLeft()).to.be(true); + }); + it(`should return a right on a glob that does indeed expand`, () => { + const aPath = 'src/dev/code_coverage/ingest_coverage/*.js'; + const actual = tryPath(aPath); + expect(actual.isRight()).to.be(true); + }); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js index 7ca7279e0d64ce..f668c1f86f5b0d 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js @@ -18,13 +18,8 @@ */ import expect from '@kbn/expect'; -import { maybeTeamAssign, whichIndex } from '../ingest_helpers'; -import { - TOTALS_INDEX, - RESEARCH_TOTALS_INDEX, - RESEARCH_COVERAGE_INDEX, - // COVERAGE_INDEX, -} from '../constants'; +import { whichIndex } from '../ingest_helpers'; +import { TOTALS_INDEX, RESEARCH_TOTALS_INDEX, RESEARCH_COVERAGE_INDEX } from '../constants'; describe(`Ingest Helper fns`, () => { describe(`whichIndex`, () => { @@ -56,20 +51,4 @@ describe(`Ingest Helper fns`, () => { }); }); }); - describe(`maybeTeamAssign`, () => { - describe(`against a coverage index`, () => { - it(`should have the pipeline prop`, () => { - const actual = maybeTeamAssign(true, { a: 'blah' }); - expect(actual).to.have.property('pipeline'); - }); - }); - describe(`against a totals index`, () => { - describe(`for "prod"`, () => { - it(`should not have the pipeline prop`, () => { - const actual = maybeTeamAssign(false, { b: 'blah' }); - expect(actual).not.to.have.property('pipeline'); - }); - }); - }); - }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json deleted file mode 100644 index 355c484a84fa35..00000000000000 --- a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "abc": "123" -} diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt new file mode 100644 index 00000000000000..d8924bd563f301 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt @@ -0,0 +1,194 @@ +x-pack/plugins/dashboard_enhanced/public/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/mocks.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/plugin.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/public/services/index.ts kibana-app +x-pack/plugins/dashboard_enhanced/scripts/storybook.js kibana-app +x-pack/plugins/discover_enhanced/common/config.ts kibana-app +x-pack/plugins/discover_enhanced/common/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts kibana-app +x-pack/plugins/discover_enhanced/public/actions/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/index.ts kibana-app +x-pack/plugins/discover_enhanced/public/plugin.ts kibana-app +x-pack/plugins/discover_enhanced/server/config.ts kibana-app +x-pack/plugins/discover_enhanced/server/index.ts kibana-app +x-pack/plugins/discover_enhanced/server/plugin.ts kibana-app +x-pack/plugins/lens/common/api.ts kibana-app +x-pack/plugins/lens/common/constants.ts kibana-app +x-pack/plugins/lens/common/index.ts kibana-app +x-pack/plugins/lens/common/types.ts kibana-app +x-pack/plugins/lens/config.ts kibana-app +x-pack/plugins/lens/public/app_plugin/app.test.tsx kibana-app +x-pack/plugins/lens/public/app_plugin/app.tsx kibana-app +x-pack/plugins/lens/public/app_plugin/index.ts kibana-app +x-pack/plugins/lens/public/app_plugin/mounter.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/expression.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/index.ts kibana-app +x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx kibana-app +x-pack/plugins/lens/public/datatable_visualization/visualization.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/debounced_component.tsx kibana-app +x-pack/plugins/lens/public/debounced_component/index.ts kibana-app +x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/drag_drop.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/index.ts kibana-app +x-pack/plugins/lens/public/drag_drop/providers.test.tsx kibana-app +x-pack/plugins/lens/public/drag_drop/providers.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/format_column.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/index.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts kibana-app +x-pack/plugins/lens/public/editor_frame_service/mocks.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/service.test.tsx kibana-app +x-pack/plugins/lens/public/editor_frame_service/service.tsx kibana-app +x-pack/plugins/lens/public/help_menu_util.tsx kibana-app +x-pack/plugins/lens/public/id_generator/id_generator.test.ts kibana-app +x-pack/plugins/lens/public/id_generator/id_generator.ts kibana-app +x-pack/plugins/lens/public/id_generator/index.ts kibana-app +x-pack/plugins/lens/public/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx kibana-app +x-pack/plugins/reporting/server/browsers/download/clean.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/download.test.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/download.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/index.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/download/util.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/extract_error.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/index.js kibana-reporting +x-pack/plugins/reporting/server/browsers/extract/unzip.js kibana-reporting +x-pack/plugins/reporting/server/browsers/index.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/install.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/network_policy.test.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/network_policy.ts kibana-reporting +x-pack/plugins/reporting/server/browsers/safe_child_process.ts kibana-reporting +x-pack/plugins/reporting/server/config/config.ts kibana-reporting +x-pack/plugins/reporting/server/config/create_config.test.ts kibana-reporting +x-pack/plugins/reporting/server/config/create_config.ts kibana-reporting +x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts kibana-reporting +x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/loader.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/types.ts kibana-app +x-pack/plugins/lens/public/indexpattern_datasource/utils.ts kibana-app +x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts kibana-app diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.test.js deleted file mode 100644 index e597ffb5d2f4be..00000000000000 --- a/src/dev/code_coverage/ingest_coverage/__tests__/team_assignment.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 expect from '@kbn/expect'; -import { fetch } from '../team_assignment/get_data'; -import { noop } from '../utils'; - -describe(`Team Assignment`, () => { - const mockPath = 'src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.json'; - describe(`fetch fn`, () => { - it(`should be a fn`, () => { - expect(typeof fetch).to.be('function'); - }); - describe(`applied to a path that exists`, () => { - it(`should return the contents of the path`, () => { - const sut = fetch(mockPath); - expect(sut.chain(JSON.parse)).to.have.property('abc'); - }); - }); - describe(`applied to an non-existing path`, () => { - it(`should return a Left with the error message within`, () => { - const expectLeft = (err) => - expect(err.message).to.contain('ENOENT: no such file or directory'); - - fetch('fake_path.json').fold(expectLeft, noop); - }); - }); - }); -}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 746bccc3d718a4..b6d17f83e327ed 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -18,9 +18,17 @@ */ import expect from '@kbn/expect'; -import { ciRunUrl, coveredFilePath, itemizeVcs, prokPrevious } from '../transforms'; +import { + ciRunUrl, + coveredFilePath, + itemizeVcs, + prokPrevious, + teamAssignment, + last, +} from '../transforms'; +import { ToolingLog } from '@kbn/dev-utils'; -describe(`Transform fn`, () => { +describe(`Transform fns`, () => { describe(`ciRunUrl`, () => { it(`should add the url when present in the environment`, () => { process.env.CI_RUN_URL = 'blah'; @@ -83,4 +91,59 @@ describe(`Transform fn`, () => { ); }); }); + describe(`teamAssignment`, () => { + const teamAssignmentsPathMOCK = + 'src/dev/code_coverage/ingest_coverage/__tests__/mocks/team_assign_mock.txt'; + const coveredFilePath = 'x-pack/plugins/reporting/server/browsers/extract/unzip.js'; + const obj = { coveredFilePath }; + const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + + describe(`with a coveredFilePath of ${coveredFilePath}`, () => { + const expected = 'kibana-reporting'; + it(`should resolve to ${expected}`, async () => { + const actual = await teamAssignment(teamAssignmentsPathMOCK)(log)(obj); + const { team } = actual; + expect(team).to.eql(expected); + }); + }); + + describe(`with a coveredFilePath of src/plugins/charts/public/static/color_maps/color_maps.ts`, () => { + const expected = 'kibana-reporting'; + it(`should resolve to ${expected}`, async () => { + const actual = await teamAssignment(teamAssignmentsPathMOCK)(log)(obj); + const { team } = actual; + expect(team).to.eql(expected); + }); + }); + + describe(`last fn`, () => { + describe(`applied to n results`, () => { + it(`should pick the last one`, () => { + const nteams = `src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app +src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch`; + + const actual = last(nteams); + + expect(actual).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch' + ); + }); + }); + describe(`applied to 1 result`, () => { + it(`should pick that 1 result`, () => { + const nteams = + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch'; + + const actual = last(nteams); + + expect(actual).to.be( + 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app-arch' + ); + }); + }); + }); + }); }); diff --git a/src/dev/code_coverage/ingest_coverage/constants.js b/src/dev/code_coverage/ingest_coverage/constants.js index f2f467e461ae52..8f850ac2f1f122 100644 --- a/src/dev/code_coverage/ingest_coverage/constants.js +++ b/src/dev/code_coverage/ingest_coverage/constants.js @@ -27,8 +27,6 @@ export const RESEARCH_COVERAGE_INDEX = export const RESEARCH_TOTALS_INDEX = process.env.RESEARCH_TOTALS_INDEX || `qa_research_total_code_coverage`; -export const TEAM_ASSIGNMENT_PIPELINE_NAME = process.env.PIPELINE_NAME || 'team_assignment'; - export const CODE_COVERAGE_CI_JOB_NAME = 'elastic+kibana+code-coverage'; export const RESEARCH_CI_JOB_NAME = 'elastic+kibana+qa-research'; export const CI_JOB_NAME = process.env.COVERAGE_JOB_NAME || RESEARCH_CI_JOB_NAME; diff --git a/src/dev/code_coverage/ingest_coverage/either.js b/src/dev/code_coverage/ingest_coverage/either.js index eeb48893f18d83..326f238074e304 100644 --- a/src/dev/code_coverage/ingest_coverage/either.js +++ b/src/dev/code_coverage/ingest_coverage/either.js @@ -20,11 +20,15 @@ /* eslint new-cap: 0 */ /* eslint no-unused-vars: 0 */ +import { always } from './utils'; + export const Right = (x) => ({ chain: (f) => f(x), map: (f) => Right(f(x)), fold: (leftFn, rightFn) => rightFn(x), inspect: () => `Right(${x})`, + isLeft: always(false), + isRight: always(true), }); Right.of = function of(x) { @@ -40,6 +44,8 @@ export const Left = (x) => ({ map: (f) => Left(x), fold: (leftFn, rightFn) => leftFn(x), inspect: () => `Left(${x})`, + isLeft: always(true), + isRight: always(false), }); Left.of = function of(x) { diff --git a/src/dev/code_coverage/ingest_coverage/index.js b/src/dev/code_coverage/ingest_coverage/index.js index 4047ee78ee6ec5..f29739c4cf29cf 100644 --- a/src/dev/code_coverage/ingest_coverage/index.js +++ b/src/dev/code_coverage/ingest_coverage/index.js @@ -20,13 +20,16 @@ import { resolve } from 'path'; import { prok } from './process'; import { run, createFlagError } from '@kbn/dev-utils'; +import { pathExists } from './team_assignment/enumeration_helpers'; +import { id, reThrow } from './utils'; const ROOT = resolve(__dirname, '../../../..'); const flags = { - string: ['path', 'verbose', 'vcsInfoPath'], + string: ['path', 'verbose', 'vcsInfoPath', 'teamAssignmentsPath'], help: ` --path Required, path to the file to extract coverage data --vcsInfoPath Required, path to the git info file (branch, sha, author, & commit msg) +--teamAssignmentsPath Required, path to the team assignments data file `, }; @@ -36,12 +39,18 @@ export function runCoverageIngestionCli() { if (flags.path === '') throw createFlagError('please provide a single --path flag'); if (flags.vcsInfoPath === '') throw createFlagError('please provide a single --vcsInfoPath flag'); + if (flags.teamAssignmentsPath === '') + throw createFlagError('please provide a single --teamAssignments flag'); if (flags.verbose) log.verbose(`Verbose logging enabled`); const resolveRoot = resolve.bind(null, ROOT); const jsonSummaryPath = resolveRoot(flags.path); const vcsInfoFilePath = resolveRoot(flags.vcsInfoPath); - prok({ jsonSummaryPath, vcsInfoFilePath }, log); + const { teamAssignmentsPath } = flags; + + pathExists(teamAssignmentsPath).fold(reThrow, id); + + prok({ jsonSummaryPath, vcsInfoFilePath, teamAssignmentsPath }, log); }, { description: ` diff --git a/src/dev/code_coverage/ingest_coverage/ingest.js b/src/dev/code_coverage/ingest_coverage/ingest.js index 31a94d161a3ccc..24da7a9338a464 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest.js +++ b/src/dev/code_coverage/ingest_coverage/ingest.js @@ -19,7 +19,7 @@ const { Client } = require('@elastic/elasticsearch'); import { createFailError } from '@kbn/dev-utils'; -import { RESEARCH_CI_JOB_NAME, TEAM_ASSIGNMENT_PIPELINE_NAME } from './constants'; +import { RESEARCH_CI_JOB_NAME } from './constants'; import { errMsg, redact, whichIndex } from './ingest_helpers'; import { pretty, green } from './utils'; import { right, left } from './either'; @@ -34,14 +34,10 @@ const isResearchJob = process.env.COVERAGE_JOB_NAME === RESEARCH_CI_JOB_NAME ? t export const ingest = (log) => async (body) => { const isTotal = !!body.isTotal; const index = whichIndex(isResearchJob)(isTotal); - const isACoverageIndex = isTotal ? false : true; const stringified = pretty(body); - const pipeline = TEAM_ASSIGNMENT_PIPELINE_NAME; - const finalPayload = isACoverageIndex - ? { index, body: stringified, pipeline } - : { index, body: stringified }; + const finalPayload = { index, body: stringified }; const justLog = dontSendButLog(log); const doSendToIndex = doSend(index); @@ -77,11 +73,11 @@ async function send(logF, idx, redactedEsHostUrl, client, requestBody) { const sendMsg = (actuallySent, redactedEsHostUrl, payload) => { const { index, body } = payload; return `### ${actuallySent ? 'Sent' : 'Fake Sent'}: -${payload.pipeline ? `\t### Team Assignment Pipeline: ${green(payload.pipeline)}` : ''} ${redactedEsHostUrl ? `\t### ES Host: ${redactedEsHostUrl}` : ''} + \t### Index: ${green(index)} + \t### payload.body: ${body} -${process.env.NODE_ENV === 'integration_test' ? `ingest-pipe=>${payload.pipeline}` : ''} `; }; diff --git a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js index 86bcf039770829..a7822a671887b3 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js +++ b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js @@ -24,7 +24,6 @@ import { COVERAGE_INDEX, RESEARCH_COVERAGE_INDEX, RESEARCH_TOTALS_INDEX, - TEAM_ASSIGNMENT_PIPELINE_NAME, TOTALS_INDEX, } from './constants'; @@ -70,12 +69,6 @@ function color(whichColor) { }; } -export function maybeTeamAssign(isACoverageIndex, body) { - const doAddTeam = isACoverageIndex ? true : false; - const payload = doAddTeam ? { ...body, pipeline: TEAM_ASSIGNMENT_PIPELINE_NAME } : body; - return payload; -} - export function whichIndex(isResearchJob) { return (isTotal) => isTotal ? whichTotalsIndex(isResearchJob) : whichCoverageIndex(isResearchJob); diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index ba73922ec508ab..a4d07215efec1d 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -20,6 +20,7 @@ import { resolve } from 'path'; import execa from 'execa'; import expect from '@kbn/expect'; +import shell from 'shelljs'; const ROOT_DIR = resolve(__dirname, '../../../../..'); const MOCKS_DIR = resolve(__dirname, './mocks'); @@ -35,9 +36,14 @@ const env = { }; describe('Ingesting coverage', () => { + const teamAssignmentsPath = + 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt'; + const verboseArgs = [ 'scripts/ingest_coverage.js', '--verbose', + '--teamAssignmentsPath', + teamAssignmentsPath, '--vcsInfoPath', 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', '--path', @@ -46,6 +52,21 @@ describe('Ingesting coverage', () => { const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; const resolved = resolve(MOCKS_DIR, summaryPath); + beforeAll(async () => { + const params = [ + 'scripts/generate_team_assignments.js', + '--src', + '.github/CODEOWNERS', + '--dest', + teamAssignmentsPath, + ]; + await execa(process.execPath, params, { cwd: ROOT_DIR, env }); + }); + + afterAll(() => { + shell.rm(teamAssignmentsPath); + }); + describe(`staticSiteUrl`, () => { let actualUrl = ''; const siteUrlRegex = /"staticSiteUrl":\s*(.+,)/; diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS new file mode 100644 index 00000000000000..1822c3fd95e343 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS @@ -0,0 +1,6 @@ +# GitHub CODEOWNERS definition +# Identify which groups will be pinged by changes to different parts of the codebase. +# For more info, see https://help.github.com/articles/about-codeowners/ + +# App +/x-pack/plugins/code/ @elastic/kibana-tre diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json new file mode 100644 index 00000000000000..9e66d8f5cb1018 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-possibly-n-teams.json @@ -0,0 +1,28 @@ +{ + "/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/src/plugins/charts/public/static/color_maps/color_maps.ts": { + "lines": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "functions": { + "total": 1, + "covered": 1, + "skipped": 0, + "pct": 100 + }, + "statements": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "branches": { + "total": 0, + "covered": 0, + "skipped": 0, + "pct": 100 + } + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json new file mode 100644 index 00000000000000..6e4d8ea954c2c3 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-qa-research-job.json @@ -0,0 +1,28 @@ +{ + "/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js": { + "lines": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "functions": { + "total": 1, + "covered": 1, + "skipped": 0, + "pct": 100 + }, + "statements": { + "total": 4, + "covered": 4, + "skipped": 0, + "pct": 100 + }, + "branches": { + "total": 0, + "covered": 0, + "skipped": 0, + "pct": 100 + } + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js new file mode 100644 index 00000000000000..c666581ddb08ce --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES 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 execa from 'execa'; +import expect from '@kbn/expect'; +import shell from 'shelljs'; + +const ROOT_DIR = resolve(__dirname, '../../../../..'); +const MOCKS_DIR = resolve(__dirname, './mocks'); + +describe('Team Assignment', () => { + const teamAssignmentsPath = + 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt'; + const mockCodeOwners = 'CODEOWNERS'; + const resolved = resolve(MOCKS_DIR, mockCodeOwners); + + beforeAll(async () => { + const params = [ + 'scripts/generate_team_assignments.js', + '--src', + resolved, + '--dest', + teamAssignmentsPath, + ]; + await execa(process.execPath, params, { cwd: ROOT_DIR }); + }); + + afterAll(() => { + shell.rm(teamAssignmentsPath); + }); + + describe(`when the codeowners file contains #CC#`, () => { + it(`should strip the prefix and still drill down through the fs`, async () => { + const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); + expect(stdout).to.be(`x-pack/plugins/code/server/config.ts kibana-tre +x-pack/plugins/code/server/index.ts kibana-tre +x-pack/plugins/code/server/plugin.test.ts kibana-tre +x-pack/plugins/code/server/plugin.ts kibana-tre`); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/process.js b/src/dev/code_coverage/ingest_coverage/process.js index 85a42cfffa6e2c..28a7c9ccd41b08 100644 --- a/src/dev/code_coverage/ingest_coverage/process.js +++ b/src/dev/code_coverage/ingest_coverage/process.js @@ -18,7 +18,7 @@ */ import { fromEventPattern, of, fromEvent } from 'rxjs'; -import { concatMap, delay, map, takeUntil } from 'rxjs/operators'; +import { concatMap, delay, map, mergeMap, takeUntil } from 'rxjs/operators'; import jsonStream from './json_stream'; import { pipe, noop, green, always } from './utils'; import { ingest } from './ingest'; @@ -32,6 +32,7 @@ import { coveredFilePath, ciRunUrl, itemizeVcs, + teamAssignment, } from './transforms'; import { resolve } from 'path'; import { createReadStream } from 'fs'; @@ -50,9 +51,10 @@ const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP || formatte const preamble = pipe(statsAndstaticSiteUrl, rootDirAndOrigPath, buildId, addPrePopulatedTimeStamp); const addTestRunnerAndStaticSiteUrl = pipe(testRunner, staticSite(staticSiteUrlBase)); -const transform = (jsonSummaryPath) => (log) => (vcsInfo) => { +const transform = (jsonSummaryPath) => (log) => (vcsInfo) => (teamAssignmentsPath) => { const objStream = jsonStream(jsonSummaryPath).on('done', noop); const itemizeVcsInfo = itemizeVcs(vcsInfo); + const assignTeams = teamAssignment(teamAssignmentsPath)(log); const jsonSummary$ = (_) => objStream.on('node', '!.*', _); @@ -64,6 +66,7 @@ const transform = (jsonSummaryPath) => (log) => (vcsInfo) => { map(ciRunUrl), map(addJsonSummaryPath(jsonSummaryPath)), map(addTestRunnerAndStaticSiteUrl), + mergeMap(assignTeams), concatMap((x) => of(x).pipe(delay(ms))) ) .subscribe(ingest(log)); @@ -83,7 +86,7 @@ const vcsInfoLines$ = (vcsInfoFilePath) => { return fromEvent(rl, 'line').pipe(takeUntil(fromEvent(rl, 'close'))); }; -export const prok = ({ jsonSummaryPath, vcsInfoFilePath }, log) => { +export const prok = ({ jsonSummaryPath, vcsInfoFilePath, teamAssignmentsPath }, log) => { validateRoot(COVERAGE_INGESTION_KIBANA_ROOT, log); logAll(jsonSummaryPath, log); @@ -93,7 +96,7 @@ export const prok = ({ jsonSummaryPath, vcsInfoFilePath }, log) => { vcsInfoLines$(vcsInfoFilePath).subscribe( mutateVcsInfo(vcsInfo), (err) => log.error(err), - always(xformWithPath(vcsInfo)) + always(xformWithPath(vcsInfo)(teamAssignmentsPath)) ); }; diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js b/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js new file mode 100644 index 00000000000000..dcdb32c91b8f8a --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/enumerate_patterns.js @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import { + push, + prokGlob, + trim, + isRejectedDir, + isFileAllowed, + isDir, + tryPath, + dropEmpty, + notFound, +} from './enumeration_helpers'; +import { stripLeading } from '../transforms'; + +export const enumeratePatterns = (rootPath) => (log) => (patterns) => { + const res = []; + const resPush = push(res); + const logNotFound = notFound(log); + + for (const entry of patterns) { + const [pathPattern, team] = entry; + const cleaned = stripLeading(pathPattern); + const existsWithOwner = pathExists(team); + + const collect = (x) => existsWithOwner(x).forEach(resPush); + tryPath(cleaned).fold(logNotFound, collect); + } + + return res; + + function pathExists(owner) { + const creeper = (x) => creepFsSync(x, [], rootPath, owner); + return function creepAllAsGlobs(pathPattern) { + return prokGlob(pathPattern).map(creeper).filter(dropEmpty); + }; + } +}; + +function creepFsSync(aPath, xs, rootPath, owner) { + xs = xs || []; + + const joinRoot = join.bind(null, rootPath); + const trimRoot = trim(rootPath); + const joined = joinRoot(aPath); + const isADir = isDir(joined); + + (isADir ? readdirSync(joined) : [aPath]).forEach(maybeRecurse); + + return xs; + + function maybeRecurse(entry) { + const full = isADir ? join(aPath, entry) : entry; + const fullIsDir = statSync(full).isDirectory(); + + if (fullIsDir && !isRejectedDir(full)) xs = creepFsSync(full, xs, rootPath, owner); + else if (isFileAllowed(full)) xs.push(`${trimRoot(full)} ${owner}`); + } +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js b/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js new file mode 100644 index 00000000000000..b06141965d2b66 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/enumeration_helpers.js @@ -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 { statSync } from 'fs'; +import isGlob from 'is-glob'; +import glob from 'glob'; +import { left, right, tryCatch } from '../either'; + +export const push = (xs) => (x) => xs.push(x); +export const pathExists = (x) => tryCatch(() => statSync(x)).fold(left, right); +export const isDir = (x) => statSync(x).isDirectory(); +export const prokGlob = (x) => glob.sync(x, { nonull: true }); +export const trim = (ROOT) => (x) => x.replace(`${ROOT}/`, ''); +export const isFileAllowed = (x) => { + const isJsOrTsOrTsxOrJsx = /.(j|t)(s|sx)$/gm; + return isJsOrTsOrTsxOrJsx.test(x); +}; +export const isRejectedDir = (x) => + /node_modules|__tests__|__fixture__|__fixtures__|build\//gm.test(x); +const isGlobFound = (x) => (xs) => (x === xs[0] ? false : true); +export const globExpands = (x) => isGlobFound(x)(prokGlob(x)); +export const tryPath = (x) => { + const isAGlob = isGlob(x); + + if (isAGlob) return globExpands(x) ? right(x) : left(x); + + if (!isAGlob) return pathExists(x).isRight() ? right(x) : left(x); +}; +export const dropEmpty = (x) => x.length > 0; +export const notFound = (log) => (err) => log.error(`\n!!! Not Found: \n${err}`); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js similarity index 51% rename from src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js rename to src/dev/code_coverage/ingest_coverage/team_assignment/flush.js index 22a9d0a461ebf2..5150ac2e655f22 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/flush.js @@ -17,21 +17,28 @@ * under the License. */ -import { createFailError } from '@kbn/dev-utils'; -import { ES_HOST } from '../constants'; -import { pretty, green } from '../utils'; +import { writeFileSync } from 'fs'; +import shell from 'shelljs'; +import { tryCatch } from '../either'; +import { id } from '../utils'; -const { Client } = require('@elastic/elasticsearch'); +const encoding = 'utf8'; +const appendUtf8 = { flag: 'a', encoding }; -const node = ES_HOST; -const client = new Client({ node }); +export const flush = (dest) => (log) => (assignments) => { + log.verbose(`\n### Flushing assignments to: \n\t${dest}`); -export const update = (id) => (log) => async (body) => { - try { - await client.ingest.putPipeline({ id, body }); - log.verbose(`### Ingestion Pipeline ID: ${green(id)}`); - log.verbose(`### Payload Partial: \n${body.slice(0, 600)}...`); - } catch (e) { - throw createFailError(`${pretty(e.meta)}`); - } + const writeToFile = writeFileSync.bind(null, dest); + + writeToFile('', { encoding }); + + for (const xs of assignments) xs.forEach((x) => writeToFile(`${x}\n`, appendUtf8)); + + tryCatch(() => maybeShowSize(dest)).fold(id, (x) => { + log.verbose(`\n### Flushed [${x}] lines`); + }); }; +function maybeShowSize(x) { + const { output } = shell.exec(`wc -l ${x}`, { silent: true }); + return output.match(/\s*\d*\s*/)[0].trim(); +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js index 11f97487082839..30112df3b6ba46 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -17,42 +17,55 @@ * under the License. */ -import { run } from '@kbn/dev-utils'; -import { TEAM_ASSIGNMENT_PIPELINE_NAME } from '../constants'; -import { fetch } from './get_data'; -import { update } from './update_ingest_pipeline'; +import { run, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { parse } from './parse_owners'; +import { flush } from './flush'; +import { enumeratePatterns } from './enumerate_patterns'; +import { pipe } from '../utils'; +import { reduce } from 'rxjs/operators'; -const updatePipeline = update(TEAM_ASSIGNMENT_PIPELINE_NAME); - -const execute = ({ flags, log }) => { - if (flags.verbose) log.verbose(`### Verbose logging enabled`); - - const logLeft = handleErr(log); - const updateAndLog = updatePipeline(log); - - const { path } = flags; - - fetch(path).fold(logLeft, updateAndLog); +const flags = { + string: ['src', 'dest'], + help: ` +--src Required, path to CODEOWNERS file. +--dest Required, destination path of the assignments. + `, }; -function handleErr(log) { - return (msg) => log.error(msg); -} +export const generateTeamAssignments = () => { + run( + ({ flags, log }) => { + if (flags.src === '') throw createFlagError('please provide a single --src flag'); + if (flags.dest === '') throw createFlagError('please provide a single --dest flag'); -const description = ` + const logCreepAndFlush = pipe( + logSuccess(flags.src, log), + enumeratePatterns(REPO_ROOT)(log), + flush(flags.dest)(log) + ); -Upload the latest team assignment pipeline def from src, -to the cluster. + parse(flags.src).pipe(reduce(toMap, new Map())).subscribe(logCreepAndFlush); + }, + { + description: ` - `; +Create a file defining the team assignments, + parsed from .github/CODEOWNERS -const flags = { - string: ['path', 'verbose'], - help: ` ---path Required, path to painless definition for team assignment. - `, + `, + flags, + } + ); }; -const usage = 'node scripts/load_team_assignment.js --verbose --path PATH_TO_PAINLESS_SCRIPT.json'; +function toMap(acc, x) { + acc.set(x[0], x[1][0]); + return acc; +} -export const uploadTeamAssignmentJson = () => run(execute, { description, flags, usage }); +function logSuccess(src, log) { + return (dataObj) => { + log.verbose(`\n### Parsing [${src}] Complete`); + return dataObj; + }; +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json deleted file mode 100644 index 017d208133cdc0..00000000000000 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json +++ /dev/null @@ -1 +0,0 @@ -{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/kbn-es-archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js similarity index 59% rename from src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js rename to src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js index 34526a2f793021..a07d556c9b403f 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js @@ -17,17 +17,19 @@ * under the License. */ -import { readFileSync } from 'fs'; -import { resolve } from 'path'; -import { tryCatch as tc } from '../either'; +import { fromEvent } from 'rxjs'; +import { map, filter, takeUntil } from 'rxjs/operators'; +import { lineRead, pathAndTeams, empties, comments, dropCCDelim } from './parse_owners_helpers'; +import { pipe } from '../utils'; -const ROOT = resolve(__dirname, '../../../../..'); +const cleanAndParse = pipe(dropCCDelim, pathAndTeams); -const resolveFromRoot = resolve.bind(null, ROOT); +const allLines$ = (lineReader) => + fromEvent(lineReader, 'line').pipe( + filter(empties), + filter(comments), + map(cleanAndParse), + takeUntil(fromEvent(lineReader, 'close')) + ); -const resolved = (path) => () => resolveFromRoot(path); - -const getContents = (path) => tc(() => readFileSync(path, 'utf8')); - -// fetch :: String -> Left | Right -export const fetch = (path) => tc(resolved(path)).chain(getContents); +export const parse = (codeOwnersPath) => allLines$(lineRead(codeOwnersPath)); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.js b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.js new file mode 100644 index 00000000000000..454accb00a7b66 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners_helpers.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 { always, id, pipe } from '../utils'; +import * as Either from '../either'; +import readline from 'readline'; +import { createReadStream } from 'fs'; +import { pluckIndex } from '../transforms'; + +const coverageDelimRe = /^#CC#\s/; + +export const empties = (x) => x !== ''; +export const comments = (x) => !/^#\s{1,3}/.test(x); +const dropDelim = (x) => () => x.replace(coverageDelimRe, ''); + +export const dropCCDelim = (x) => + Either.fromNullable(coverageDelimRe.test(x)).fold(always(x), id(dropDelim(x))); + +const splitFilter = (splitter) => (x) => x.split(splitter).filter(empties); +const spaceSplit = splitFilter(' '); +const esSplit = splitFilter('@elastic/'); +const getFirst = pluckIndex(0); +const trimEsGrabFirst = pipe(esSplit, getFirst); + +export const pathAndTeams = (x) => { + const [path, ...teamEntries] = spaceSplit(x); + const teams = teamEntries.map(trimEsGrabFirst); + + return [path, teams]; +}; + +export const lineRead = (x) => readline.createInterface({ input: createReadStream(x) }); diff --git a/examples/alerting_example/common/constants.ts b/src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js similarity index 67% rename from examples/alerting_example/common/constants.ts rename to src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js index 5884eb32205195..50dd6f719f34e7 100644 --- a/examples/alerting_example/common/constants.ts +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parsing_helpers.js @@ -17,18 +17,14 @@ * under the License. */ -export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; +import { always } from '../utils'; +import * as Either from '../either'; -// always firing -export const DEFAULT_INSTANCES_TO_GENERATE = 5; +const coverageDelimRe = /^#CC#\s/; -// Astros -export enum Craft { - OuterSpace = 'Outer Space', - ISS = 'ISS', -} -export enum Operator { - AreAbove = 'Are above', - AreBelow = 'Are below', - AreExactly = 'Are exactly', -} +export const empties = (x) => x !== ''; +export const comments = (x) => !/^#\s{1,3}/.test(x); +const dropDelim = (x) => x.replace(coverageDelimRe, ''); + +export const dropCCDelim = (x) => + Either.fromNullable(coverageDelimRe.test(x)).fold(always(x), always(dropDelim(x))); diff --git a/src/dev/code_coverage/ingest_coverage/transforms.js b/src/dev/code_coverage/ingest_coverage/transforms.js index b8c9acd6fc49d6..ff6ffd46462ed9 100644 --- a/src/dev/code_coverage/ingest_coverage/transforms.js +++ b/src/dev/code_coverage/ingest_coverage/transforms.js @@ -18,8 +18,12 @@ */ import * as Either from './either'; -import { fromNullable } from './maybe'; -import { always, id, noop } from './utils'; +import * as Maybe from './maybe'; +import { always, id, noop, pink } from './utils'; +import execa from 'execa'; +import { resolve } from 'path'; + +const ROOT_DIR = resolve(__dirname, '../../../..'); const maybeTotal = (x) => (x === 'total' ? Either.left(x) : Either.right(x)); @@ -83,20 +87,60 @@ export const staticSite = (urlBase) => (obj) => { return { ...obj, staticSiteUrl: prokForBoth() }; }; +const leadingSlashRe = /^\//; +export const maybeDropLeadingSlash = (x) => + leadingSlashRe.test(x) ? Either.right(x) : Either.left(x); +export const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); +export const stripLeading = (x) => maybeDropLeadingSlash(x).fold(id, dropLeadingSlash); + export const coveredFilePath = (obj) => { const { staticSiteUrl, COVERAGE_INGESTION_KIBANA_ROOT } = obj; const withoutCoveredFilePath = always(obj); - const leadingSlashRe = /^\//; - const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? Either.right(x) : Either.left(x)); - const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); - const dropRoot = (root) => (x) => - maybeDropLeadingSlash(x.replace(root, '')).fold(id, dropLeadingSlash); + const dropRoot = (root) => (x) => stripLeading(x.replace(root, '')); return maybeTotal(staticSiteUrl) .map(dropRoot(COVERAGE_INGESTION_KIBANA_ROOT)) .fold(withoutCoveredFilePath, (coveredFilePath) => ({ ...obj, coveredFilePath })); }; +const findTeam = (x) => x.match(/.+\s{1,3}(.+)$/, 'gm'); +export const pluckIndex = (idx) => (xs) => xs[idx]; +const pluckTeam = pluckIndex(1); + +export const teamAssignment = (teamAssignmentsPath) => (log) => async (obj) => { + const { coveredFilePath } = obj; + const isTotal = Either.fromNullable(obj.isTotal); + + return isTotal.isRight() ? obj : await assignTeam(teamAssignmentsPath, coveredFilePath, log, obj); +}; +export const last = (x) => { + const xs = x.split('\n'); + const len = xs.length; + + return len === 1 ? xs[0] : xs[len - 1]; +}; +async function assignTeam(teamAssignmentsPath, coveredFilePath, log, obj) { + const params = [coveredFilePath, teamAssignmentsPath]; + + let grepResponse; + + try { + const { stdout } = await execa('grep', params, { cwd: ROOT_DIR }); + grepResponse = stdout; + } catch (e) { + log.error(`\n!!! Unknown Team for path: \n\t\t${pink(coveredFilePath)}\n`); + } + + return Either.fromNullable(grepResponse) + .map(last) + .map(findTeam) + .map(pluckTeam) + .fold( + () => ({ team: 'unknown', ...obj }), + (team) => ({ team, ...obj }) + ); +} + export const ciRunUrl = (obj) => Either.fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ ...obj, @@ -126,13 +170,12 @@ export const itemizeVcs = (vcsInfo) => (obj) => { }; const mutateVcs = (x) => (vcs.commitMsg = truncateMsg(x)); - fromNullable(commitMsg).map(mutateVcs); + Maybe.fromNullable(commitMsg).map(mutateVcs); const vcsCompareUrl = process.env.FETCHED_PREVIOUS ? `${comparePrefix()}/${process.env.FETCHED_PREVIOUS}...${sha}` : 'PREVIOUS SHA NOT PROVIDED'; - // const withoutPreviousL = always({ ...obj, vcs }); const withPreviousR = () => ({ ...obj, vcs: { diff --git a/src/dev/code_coverage/ingest_coverage/utils.js b/src/dev/code_coverage/ingest_coverage/utils.js index 7d817bdf7a6f39..e854e3d3b72481 100644 --- a/src/dev/code_coverage/ingest_coverage/utils.js +++ b/src/dev/code_coverage/ingest_coverage/utils.js @@ -22,6 +22,10 @@ import chalk from 'chalk'; export const pipe = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))); export const noop = () => {}; export const green = (x) => chalk.greenBright.bold(x); +export const pink = (x) => chalk.bgMagenta.bold.cyan.bold(x); export const id = (x) => x; -export const always = (x) => () => x; +export const always = (x) => () => x; // Wraps a value in a fn. Eager evaluation if passed a fn. export const pretty = (x) => JSON.stringify(x, null, 2); +export const reThrow = (e) => { + throw e; +}; diff --git a/src/dev/code_coverage/shell_scripts/assign_teams.sh b/src/dev/code_coverage/shell_scripts/assign_teams.sh deleted file mode 100644 index aaa14655a9a265..00000000000000 --- a/src/dev/code_coverage/shell_scripts/assign_teams.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "### Code Coverage Team Assignment" -echo "" - -PIPELINE_NAME=$1 -export PIPELINE_NAME - -ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}" -export ES_HOST - -node scripts/load_team_assignment.js --verbose --path src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json - -echo "### Code Coverage Team Assignment - Complete" -echo "" diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh similarity index 78% rename from src/dev/code_coverage/shell_scripts/ingest_coverage.sh rename to src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 0b67dac3074738..62b81929ae79b6 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -27,19 +27,24 @@ export STATIC_SITE_URL_BASE DELAY=100 export DELAY +TEAM_ASSIGN_PATH=$5 + +# Build team assignments dat file +node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH + for x in jest functional; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json - node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH done # Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana -node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt +node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH echo "### Ingesting Code Coverage - Complete" echo "" diff --git a/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts index 0ec8d2e15c34a8..b925696c965634 100644 --- a/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts +++ b/src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts @@ -41,8 +41,9 @@ export const myCollector = makeUsageCollector({ return { something: { count_2: 2 } }; }, schema: { + // @ts-expect-error Intentionally missing count_2 something: { - count_1: { type: 'long' }, // Intentionally missing count_2 + count_1: { type: 'long' }, }, }, }); diff --git a/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts new file mode 100644 index 00000000000000..af9fef0bbd2970 --- /dev/null +++ b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts @@ -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 { CollectorSet, MakeSchemaFrom } from '../../plugins/usage_collection/server/collector'; +import { loggerMock } from '../../core/server/logging/logger.mock'; + +const { makeUsageCollector } = new CollectorSet({ + logger: loggerMock.create(), + maximumWaitTimeForAllCollectorsInS: 0, +}); + +interface MyObject { + total: number; + type: boolean; +} + +interface Usage { + flat?: string; + my_str?: string; + my_objects: MyObject; +} + +const SOME_NUMBER: number = 123; + +const someSchema: MakeSchemaFrom> = { + flat: { + type: 'keyword', + }, + my_str: { + type: 'text', + }, +}; + +const someOtherSchema: MakeSchemaFrom> = { + my_objects: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, + }, +}; + +export const myCollector = makeUsageCollector({ + type: 'schema_defined_with_spreads', + isReady: () => true, + fetch() { + const testString = '123'; + + return { + flat: 'hello', + my_str: testString, + my_objects: { + total: SOME_NUMBER, + type: true, + }, + }; + }, + schema: { + ...someSchema, + ...someOtherSchema, + }, +}); diff --git a/src/plugins/advanced_settings/public/index.ts b/src/plugins/advanced_settings/public/index.ts index db478fa1579e62..0e621e7cd7d1a4 100644 --- a/src/plugins/advanced_settings/public/index.ts +++ b/src/plugins/advanced_settings/public/index.ts @@ -17,11 +17,19 @@ * under the License. */ +import React from 'react'; import { PluginInitializerContext } from 'kibana/public'; import { AdvancedSettingsPlugin } from './plugin'; export { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; export { ComponentRegistry } from './component_registry'; -export { Field } from './management_app/components/field'; + +/** + * Exports the field component as a React.lazy component. We're explicitly naming it lazy here + * so any plugin that would import that can clearly see it's lazy loaded and can only be used + * inside a suspense context. + */ +const LazyField = React.lazy(() => import('./management_app/components/field')); +export { LazyField }; export function plugin(initializerContext: PluginInitializerContext) { return new AdvancedSettingsPlugin(); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts index d1b9b34515532e..c486dc96bfc335 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/index.ts +++ b/src/plugins/advanced_settings/public/management_app/components/field/index.ts @@ -18,3 +18,6 @@ */ export { Field, getEditableValue } from './field'; + +// eslint-disable-next-line import/no-default-export +export { Field as default } from './field'; diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index d5039d0adb3180..09fb4fae3f201e 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -18,7 +18,6 @@ */ import { ExpressionValue, ExpressionValueError } from '../expression_types'; -import { ExpressionFunction } from '../../common'; export type ExpressionAstNode = | ExpressionAstExpression @@ -48,9 +47,9 @@ export interface ExpressionAstFunctionDebug { success: boolean; /** - * Reference to the expression function this AST node represents. + * Id of expression function. */ - fn: ExpressionFunction; + fn: string; /** * Input that expression function received as its first argument. diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 2b8aa4b5e68f09..ff331d7c5ddaff 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -491,7 +491,7 @@ describe('Execution', () => { await execution.result; for (const node of execution.state.get().ast.chain) { - expect(node.debug?.fn.name).toBe('add'); + expect(node.debug?.fn).toBe('add'); } }); @@ -667,7 +667,7 @@ describe('Execution', () => { expect(node2.debug).toMatchObject({ success: false, - fn: expect.any(Object), + fn: 'throws', input: expect.any(Object), args: expect.any(Object), error: expect.any(Object), diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 3533500a2fbc59..d4c9b0a25d45b7 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -235,7 +235,7 @@ export class Execution< const timeEnd: number = now(); (link as ExpressionAstFunction).debug = { success: true, - fn, + fn: fn.name, input, args: resolvedArgs, output, @@ -253,7 +253,7 @@ export class Execution< if (this.params.debug) { (link as ExpressionAstFunction).debug = { success: false, - fn, + fn: fn.name, input, args, error, diff --git a/src/plugins/home/public/application/components/app_navigation_handler.ts b/src/plugins/home/public/application/components/app_navigation_handler.ts index 61d85c033b5448..91407ffcaf2265 100644 --- a/src/plugins/home/public/application/components/app_navigation_handler.ts +++ b/src/plugins/home/public/application/components/app_navigation_handler.ts @@ -24,12 +24,6 @@ export const createAppNavigationHandler = (targetUrl: string) => (event: MouseEv if (event.altKey || event.metaKey || event.ctrlKey) { return; } - if (targetUrl.startsWith('/app/')) { - const [, appId, path] = /\/app\/(.*?)((\/|\?|#|$).*)/.exec(targetUrl) || []; - if (!appId) { - return; - } - event.preventDefault(); - getServices().application.navigateToApp(appId, { path }); - } + event.preventDefault(); + getServices().application.navigateToUrl(targetUrl); }; diff --git a/src/plugins/telemetry/server/collectors/usage/schema.ts b/src/plugins/telemetry/server/collectors/usage/schema.ts index 8f4d555d75c494..4bfb6f75c7c8fb 100644 --- a/src/plugins/telemetry/server/collectors/usage/schema.ts +++ b/src/plugins/telemetry/server/collectors/usage/schema.ts @@ -31,7 +31,7 @@ const licenseSchema: MakeSchemaFrom = { max_resource_units: { type: 'long' }, }; -export const staticTelemetrySchema: MakeSchemaFrom> = { +export const staticTelemetrySchema: MakeSchemaFrom = { ece: { kb_uuid: { type: 'keyword' }, es_uuid: { type: 'keyword' }, diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index ab29656c557c2a..bed1bbeabb0449 100644 --- 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 @@ -49,7 +49,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` - `; -exports[`TelemetryManagementSectionComponent renders null because query does not match the SEARCH_TERMS 1`] = ` - -`; - exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `null`; 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 index c13f639f314475..0e2855f055540a 100644 --- 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 @@ -21,6 +21,7 @@ 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 { render } from '@testing-library/react'; describe('TelemetryManagementSectionComponent', () => { const coreStart = coreMock.createStart(); @@ -73,19 +74,31 @@ describe('TelemetryManagementSectionComponent', () => { http: coreSetup.http, }); - const component = mountWithIntl( - + const component = render( + Fallback}> + + ); + try { - expect( - component.setProps({ ...component.props(), query: { text: 'asssdasdsad' } }) - ).toMatchSnapshot(); + component.rerender( + Fallback}> + + + ); expect(onQueryMatchChange).toHaveBeenCalledWith(false); expect(onQueryMatchChange).toHaveBeenCalledTimes(1); } finally { 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 db104e9c7baabc..9ae0a3d12fbb52 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 @@ -34,7 +34,7 @@ import { i18n } from '@kbn/i18n'; import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; -import { Field } from '../../../advanced_settings/public'; +import { LazyField } from '../../../advanced_settings/public'; import { ToastsStart } from '../../../../core/public'; type TelemetryService = TelemetryPluginSetup['telemetryService']; @@ -119,7 +119,7 @@ export class TelemetryManagementSection extends Component { {this.maybeGetAppliesSettingMessage()} - = U extends object ? MakeSchemaFrom : { type: AllowedSchemaTypes }; +// Using Required to enforce all optional keys in the object export type MakeSchemaFrom = { - [Key in keyof Base]: Base[Key] extends Array + [Key in keyof Required]: Required[Key] extends Array ? { type: 'array'; items: RecursiveMakeSchemaFrom } - : RecursiveMakeSchemaFrom; + : RecursiveMakeSchemaFrom[Key]>; }; export interface CollectorOptions { type: string; init?: Function; - schema?: MakeSchemaFrom>; // Using Required to enforce all optional keys in the object + schema?: MakeSchemaFrom; fetch: (callCluster: LegacyAPICaller, esClient?: ElasticsearchClient) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index c14148d4a020fd..2434285bd94c6a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -163,14 +163,9 @@ export class TimeseriesVisualization extends Component { const mainAxisGroupId = yAxisIdGenerator('main_group'); const seriesModel = model.series.filter((s) => !s.hidden).map((s) => cloneDeep(s)); - const firstSeries = seriesModel.find((s) => s.formatter && !s.separate_axis); const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model); const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model); - const tickFormatter = TimeseriesVisualization.getTickFormatter( - firstSeries, - this.props.getConfig - ); const yAxis = []; let mainDomainAdded = false; @@ -203,7 +198,7 @@ export class TimeseriesVisualization extends Component { series .filter((r) => startsWith(r.id, seriesGroup.id)) .forEach((seriesDataRow) => { - seriesDataRow.tickFormatter = seriesGroupTickFormatter; + seriesDataRow.tickFormat = seriesGroupTickFormatter; seriesDataRow.groupId = groupId; seriesDataRow.yScaleType = yScaleType; seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); @@ -224,7 +219,7 @@ export class TimeseriesVisualization extends Component { }); } else if (!mainDomainAdded) { TimeseriesVisualization.addYAxis(yAxis, { - tickFormatter, + tickFormatter: series.length === 1 ? undefined : (val) => val, id: yAxisIdGenerator('main'), groupId: mainAxisGroupId, position: model.axis_position, diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js index 300af551e50203..561c985b3b5c32 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js @@ -42,6 +42,7 @@ export function AreaSeriesDecorator({ sortIndex, y1AccessorFormat, y0AccessorFormat, + tickFormat, }) { const id = seriesId; const groupId = seriesGroupId; @@ -67,6 +68,7 @@ export function AreaSeriesDecorator({ enableHistogramMode, useDefaultGroupDomain, sortIndex, + tickFormat, ...areaSeriesStyle, }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js index 239f1d4f1838e8..2d6c54de41431c 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js @@ -41,6 +41,7 @@ export function BarSeriesDecorator({ sortIndex, y1AccessorFormat, y0AccessorFormat, + tickFormat, }) { const id = seriesId; const groupId = seriesGroupId; @@ -66,6 +67,7 @@ export function BarSeriesDecorator({ enableHistogramMode, useDefaultGroupDomain, sortIndex, + tickFormat, ...barSeriesStyle, }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 5f0cc5188b1fd4..1209a105af805e 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -176,6 +176,7 @@ export const TimeSeries = ({ useDefaultGroupDomain, y1AccessorFormat, y0AccessorFormat, + tickFormat, }, sortIndex ) => { @@ -207,6 +208,7 @@ export const TimeSeries = ({ sortIndex={sortIndex} y1AccessorFormat={y1AccessorFormat} y0AccessorFormat={y0AccessorFormat} + tickFormat={tickFormat} /> ); } @@ -233,6 +235,7 @@ export const TimeSeries = ({ sortIndex={sortIndex} y1AccessorFormat={y1AccessorFormat} y0AccessorFormat={y0AccessorFormat} + tickFormat={tickFormat} /> ); } diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 44639af9da9f8f..4ca6c936143df1 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -21,21 +21,14 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'share', 'timePicker']); - const retry = getService('retry'); const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const inspector = getService('inspector'); - const docTable = getService('docTable'); - const filterBar = getService('filterBar'); - const TEST_COLUMN_NAMES = ['@message']; - const TEST_FILTER_COLUMN_NAMES = [ - ['extension', 'jpg'], - ['geo.src', 'IN'], - ]; - - // Failing: See https://github.com/elastic/kibana/issues/59975 - describe.skip('Discover', () => { + const testSubjects = getService('testSubjects'); + const TEST_COLUMN_NAMES = ['extension', 'geo.src']; + + describe('Discover a11y tests', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -46,105 +39,85 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - it('main view', async () => { + it('Discover main page', async () => { await a11y.testAppSnapshot(); }); - it('Click save button', async () => { + it('a11y test on save button', async () => { await PageObjects.discover.clickSaveSearchButton(); await a11y.testAppSnapshot(); }); - it('Save search panel', async () => { + it('a11y test on save search panel', async () => { await PageObjects.discover.inputSavedSearchTitle('a11ySearch'); await a11y.testAppSnapshot(); }); - it('Confirm saved search', async () => { + it('a11y test on clicking on confirm save', async () => { await PageObjects.discover.clickConfirmSavedSearch(); await a11y.testAppSnapshot(); }); - it('Click on new to clear the search', async () => { + it('a11y test on click new to reload discover', async () => { await PageObjects.discover.clickNewSearchButton(); await a11y.testAppSnapshot(); }); - it('Open load saved search panel', async () => { + it('a11y test on load saved search panel', async () => { await PageObjects.discover.openLoadSavedSearchPanel(); await a11y.testAppSnapshot(); await PageObjects.discover.closeLoadSavedSearchPanel(); }); - it('Open inspector panel', async () => { + it('a11y test on inspector panel', async () => { await inspector.open(); await a11y.testAppSnapshot(); await inspector.close(); }); - it('Open add filter', async () => { - await PageObjects.discover.openAddFilterPanel(); - await a11y.testAppSnapshot(); - }); - - it('Select values for a filter', async () => { - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); - await a11y.testAppSnapshot(); - }); - - it('Load a new search from the panel', async () => { - await PageObjects.discover.clickSaveSearchButton(); - await PageObjects.discover.inputSavedSearchTitle('filterSearch'); - await PageObjects.discover.clickConfirmSavedSearch(); - await PageObjects.discover.openLoadSavedSearchPanel(); - await PageObjects.discover.loadSavedSearch('filterSearch'); - await a11y.testAppSnapshot(); - }); - - it('click share button', async () => { + it('a11y test on share panel', async () => { await PageObjects.share.clickShareTopNavButton(); await a11y.testAppSnapshot(); }); - it('Open sidebar filter', async () => { + it('a11y test on open sidenav filter', async () => { await PageObjects.discover.openSidebarFieldFilter(); await a11y.testAppSnapshot(); - }); - - it('Close sidebar filter', async () => { await PageObjects.discover.closeSidebarFieldFilter(); - await a11y.testAppSnapshot(); }); - it('Add a field from sidebar', async () => { + it('a11y test on tables with columns view', async () => { for (const columnName of TEST_COLUMN_NAMES) { - await PageObjects.discover.clickFieldListItemAdd(columnName); + await PageObjects.discover.clickFieldListItemToggle(columnName); } await a11y.testAppSnapshot(); }); - it('Add more fields from sidebar', async () => { - for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - await PageObjects.discover.clickFieldListItem(columnName); - await PageObjects.discover.clickFieldListPlusFilter(columnName, value); - } + it('a11y test on save queries popover', async () => { + await PageObjects.discover.clickSavedQueriesPopOver(); await a11y.testAppSnapshot(); }); - // Context view test - it('should open context view on a doc', async () => { - await retry.try(async () => { - await docTable.clickRowToggle(); - // click the open action - const rowActions = await docTable.getRowActions(); - if (!rowActions.length) { - throw new Error('row actions empty, trying again'); - } - await rowActions[0].click(); - }); + it('a11y test on save queries panel', async () => { + await PageObjects.discover.clickCurrentSavedQuery(); await a11y.testAppSnapshot(); }); - // Adding rest of the tests after https://github.com/elastic/kibana/issues/53888 is resolved + it('a11y test on toggle include filters option on saved queries panel', async () => { + await PageObjects.discover.setSaveQueryFormTitle('test'); + await PageObjects.discover.toggleIncludeFilters(); + await a11y.testAppSnapshot(); + await PageObjects.discover.saveCurrentSavedQuery(); + }); + + // issue - https://github.com/elastic/kibana/issues/78488 + it.skip('a11y test on saved queries list panel', async () => { + await PageObjects.discover.clickSavedQueriesPopOver(); + await testSubjects.moveMouseTo( + 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' + ); + await testSubjects.find('delete-saved-query-test-button'); + await a11y.testAppSnapshot(); + }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 7a99509257bf7e..e522f41952a49a 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -119,8 +119,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async loadSavedSearch(searchName: string) { await this.openLoadSavedSearchPanel(); - const searchLink = await find.byButtonText(searchName); - await searchLink.click(); + await testSubjects.click(`savedObjectTitle${searchName.split(' ').join('-')}`); await header.waitUntilLoadingHasFinished(); } @@ -387,6 +386,42 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return await this.isDiscoverAppOnScreen(); }); } + + public async showAllFilterActions() { + await testSubjects.click('showFilterActions'); + } + + public async clickSavedQueriesPopOver() { + await testSubjects.click('saved-query-management-popover-button'); + } + + public async clickCurrentSavedQuery() { + await testSubjects.click('saved-query-management-save-button'); + } + + public async setSaveQueryFormTitle(savedQueryName: string) { + await testSubjects.setValue('saveQueryFormTitle', savedQueryName); + } + + public async toggleIncludeFilters() { + await testSubjects.click('saveQueryFormIncludeFiltersOption'); + } + + public async saveCurrentSavedQuery() { + await testSubjects.click('savedQueryFormSaveButton'); + } + + public async deleteSavedQuery() { + await testSubjects.click('delete-saved-query-TEST-button'); + } + + public async confirmDeletionOfSavedQuery() { + await testSubjects.click('confirmModalConfirmButton'); + } + + public async clearSavedQuery() { + await testSubjects.click('saved-query-management-clear-button'); + } } return new DiscoverPage(); diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh index 0c3ee4e3f261fb..59df02d401167a 100755 --- a/test/scripts/jenkins_build_plugins.sh +++ b/test/scripts/jenkins_build_plugins.sh @@ -5,7 +5,6 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --oss \ - --filter '!alertingExample' \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ --workers 6 \ diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 66ebe3478fbec6..e75ed8fef98754 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -169,31 +169,31 @@ def uploadCombinedReports() { ) } -def ingestData(jobName, buildNum, buildUrl, previousSha, title) { +def ingestData(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' ${previousSha} + . src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' '${previousSha}' '${teamAssignmentsPath}' """, title) } -def ingestWithVault(jobName, buildNum, buildUrl, previousSha, title) { +def ingestWithVault(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(jobName, buildNum, buildUrl, previousSha, title) + ingestData(jobName, buildNum, buildUrl, previousSha, teamAssignmentsPath, title) } } } } -def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmentsPath, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(jobName, buildNumber, buildUrl, previousSha, title) + ingestWithVault(jobName, buildNumber, buildUrl, previousSha, teamAssignmentsPath, title) } } diff --git a/vars/kibanaTeamAssign.groovy b/vars/kibanaTeamAssign.groovy index a3d9c16ef506ea..caf1ee36e25a83 100644 --- a/vars/kibanaTeamAssign.groovy +++ b/vars/kibanaTeamAssign.groovy @@ -1,25 +1,13 @@ -def loadIngestionPipeline(ingestionPipelineName, title) { +def generateTeamAssignments(teamAssignmentsPath, title) { kibanaPipeline.bash(""" - source src/dev/ci_setup/setup_env.sh true + source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline - . src/dev/code_coverage/shell_scripts/assign_teams.sh '${ingestionPipelineName}' - """, title) -} + # Build team assignments dat file + node scripts/generate_team_assignments.js --verbose --dest '${teamAssignmentsPath}' -def loadWithVault(ingestionPipelineName, title) { - def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' - withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { - withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { - withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - loadIngestionPipeline(ingestionPipelineName, title) - } - } - } -} - -def load(ingestionPipelineName, title) { - loadWithVault(ingestionPipelineName, title) + cat '${teamAssignmentsPath}' + """, title) } return this diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 66ae478b868280..c37a3231b2705a 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,10 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.uiActionsEnhanced": [ - "plugins/ui_actions_enhanced", - "examples/ui_actions_enhanced_examples" - ], + "xpack.uiActionsEnhanced": "plugins/ui_actions_enhanced", "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", @@ -59,6 +56,9 @@ "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, + "exclude": [ + "examples" + ], "translations": [ "plugins/translations/translations/zh-CN.json", "plugins/translations/translations/ja-JP.json" diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json index 30b2178259d682..a7aa408ba68ef2 100644 --- a/x-pack/.telemetryrc.json +++ b/x-pack/.telemetryrc.json @@ -5,7 +5,6 @@ "plugins/actions/server/usage/actions_usage_collector.ts", "plugins/alerts/server/usage/alerts_usage_collector.ts", "plugins/apm/server/lib/apm_telemetry/index.ts", - "plugins/canvas/server/collectors/collector.ts", "plugins/infra/server/usage/usage_collector.ts", "plugins/reporting/server/usage/reporting_usage_collector.ts", "plugins/maps/server/maps_telemetry/collectors/register.ts" diff --git a/examples/alerting_example/README.md b/x-pack/examples/alerting_example/README.md similarity index 100% rename from examples/alerting_example/README.md rename to x-pack/examples/alerting_example/README.md diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts new file mode 100644 index 00000000000000..dd9cc21954e613 --- /dev/null +++ b/x-pack/examples/alerting_example/common/constants.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 const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; + +// always firing +export const DEFAULT_INSTANCES_TO_GENERATE = 5; + +// Astros +export enum Craft { + OuterSpace = 'Outer Space', + ISS = 'ISS', +} +export enum Operator { + AreAbove = 'Are above', + AreBelow = 'Are below', + AreExactly = 'Are exactly', +} diff --git a/examples/alerting_example/kibana.json b/x-pack/examples/alerting_example/kibana.json similarity index 100% rename from examples/alerting_example/kibana.json rename to x-pack/examples/alerting_example/kibana.json diff --git a/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx similarity index 69% rename from examples/alerting_example/public/alert_types/always_firing.tsx rename to x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index 130519308d3c3c..839669bda1098f 100644 --- a/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -1,26 +1,13 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; interface AlwaysFiringParamsProps { diff --git a/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx similarity index 89% rename from examples/alerting_example/public/alert_types/astros.tsx rename to x-pack/examples/alerting_example/public/alert_types/astros.tsx index d52223cb6b988c..4f894cfe231c9c 100644 --- a/examples/alerting_example/public/alert_types/astros.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React, { useState, useEffect, Fragment } from 'react'; @@ -32,9 +19,9 @@ import { import { i18n } from '@kbn/i18n'; import { flatten } from 'lodash'; import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; -import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { SanitizedAlert } from '../../../../plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../plugins/alerts/public'; +import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; export function registerNavigation(alerts: AlertingSetup) { alerts.registerNavigation( diff --git a/x-pack/examples/alerting_example/public/alert_types/index.ts b/x-pack/examples/alerting_example/public/alert_types/index.ts new file mode 100644 index 00000000000000..f9a97f43e116a8 --- /dev/null +++ b/x-pack/examples/alerting_example/public/alert_types/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 { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; +import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { SanitizedAlert } from '../../../../plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../plugins/alerts/public'; + +export function registerNavigation(alerts: AlertingSetup) { + // register default navigation + alerts.registerDefaultNavigation( + ALERTING_EXAMPLE_APP_ID, + (alert: SanitizedAlert) => `/alert/${alert.id}` + ); + + registerPeopleInSpaceNavigation(alerts); +} diff --git a/examples/alerting_example/public/application.tsx b/x-pack/examples/alerting_example/public/application.tsx similarity index 73% rename from examples/alerting_example/public/application.tsx rename to x-pack/examples/alerting_example/public/application.tsx index 23e9d194410028..ebffc7d038aefb 100644 --- a/examples/alerting_example/public/application.tsx +++ b/x-pack/examples/alerting_example/public/application.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; @@ -28,14 +15,14 @@ import { DocLinksStart, ToastsSetup, ApplicationStart, -} from '../../../src/core/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; -import { ChartsPluginStart } from '../../../src/plugins/charts/public'; +} from '../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { Page } from './components/page'; import { DocumentationPage } from './components/documentation'; import { ViewAlertPage } from './components/view_alert'; -import { TriggersAndActionsUIPublicPluginStart } from '../../../x-pack/plugins/triggers_actions_ui/public'; +import { TriggersAndActionsUIPublicPluginStart } from '../../../plugins/triggers_actions_ui/public'; import { AlertingExamplePublicStartDeps } from './plugin'; import { ViewPeopleInSpaceAlertPage } from './components/view_astros_alert'; diff --git a/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx similarity index 64% rename from examples/alerting_example/public/components/create_alert.tsx rename to x-pack/examples/alerting_example/public/components/create_alert.tsx index 72e3835b100fec..c75c230e4f04ee 100644 --- a/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -1,30 +1,14 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React, { useState } from 'react'; import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; -import { - AlertsContextProvider, - AlertAdd, -} from '../../../../x-pack/plugins/triggers_actions_ui/public'; +import { AlertsContextProvider, AlertAdd } from '../../../../plugins/triggers_actions_ui/public'; import { AlertingExampleComponentParams } from '../application'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; diff --git a/examples/alerting_example/public/components/documentation.tsx b/x-pack/examples/alerting_example/public/components/documentation.tsx similarity index 63% rename from examples/alerting_example/public/components/documentation.tsx rename to x-pack/examples/alerting_example/public/components/documentation.tsx index 17cc34959b0108..73896fdb8fc924 100644 --- a/examples/alerting_example/public/components/documentation.tsx +++ b/x-pack/examples/alerting_example/public/components/documentation.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + import React from 'react'; import { diff --git a/examples/alerting_example/public/components/page.tsx b/x-pack/examples/alerting_example/public/components/page.tsx similarity index 61% rename from examples/alerting_example/public/components/page.tsx rename to x-pack/examples/alerting_example/public/components/page.tsx index 99076c7ddcedf5..e7fba53d7a3332 100644 --- a/examples/alerting_example/public/components/page.tsx +++ b/x-pack/examples/alerting_example/public/components/page.tsx @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; diff --git a/examples/alerting_example/public/components/view_alert.tsx b/x-pack/examples/alerting_example/public/components/view_alert.tsx similarity index 79% rename from examples/alerting_example/public/components/view_alert.tsx rename to x-pack/examples/alerting_example/public/components/view_alert.tsx index 0f7fc70648a9e7..6d71872d5d3f26 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_alert.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + import React, { useState, useEffect, Fragment } from 'react'; import { @@ -32,11 +20,7 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { - Alert, - AlertTaskState, - BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerts/common'; +import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx similarity index 79% rename from examples/alerting_example/public/components/view_astros_alert.tsx rename to x-pack/examples/alerting_example/public/components/view_astros_alert.tsx index b2d3cec269b729..e4687c75fa0b7b 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx @@ -1,21 +1,9 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ + import React, { useState, useEffect, Fragment } from 'react'; import { @@ -34,11 +22,7 @@ import { import { withRouter, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from 'kibana/public'; import { isEmpty } from 'lodash'; -import { - Alert, - AlertTaskState, - BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerts/common'; +import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/x-pack/examples/alerting_example/public/index.ts b/x-pack/examples/alerting_example/public/index.ts new file mode 100644 index 00000000000000..9e44d167184a9f --- /dev/null +++ b/x-pack/examples/alerting_example/public/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertingExamplePlugin } from './plugin'; + +export const plugin = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/public/plugin.tsx b/x-pack/examples/alerting_example/public/plugin.tsx similarity index 63% rename from examples/alerting_example/public/plugin.tsx rename to x-pack/examples/alerting_example/public/plugin.tsx index 3f972fa9fe2ee0..b1f8b6b99ece1f 100644 --- a/examples/alerting_example/public/plugin.tsx +++ b/x-pack/examples/alerting_example/public/plugin.tsx @@ -1,31 +1,23 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/public'; -import { ChartsPluginStart } from '../../../src/plugins/charts/public'; -import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { + Plugin, + CoreSetup, + AppMountParameters, + AppNavLinkStatus, +} from '../../../../src/core/public'; +import { PluginSetupContract as AlertingSetup } from '../../../plugins/alerts/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { getAlertType as getAlwaysFiringAlertType } from './alert_types/always_firing'; import { getAlertType as getPeopleInSpaceAlertType } from './alert_types/astros'; import { registerNavigation } from './alert_types'; -import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; export type Setup = void; export type Start = void; diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts new file mode 100644 index 00000000000000..bb1cb0d97689bf --- /dev/null +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.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 uuid from 'uuid'; +import { range } from 'lodash'; +import { AlertType } from '../../../../plugins/alerts/server'; +import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; + +export const alertType: AlertType = { + id: 'example.always-firing', + name: 'Always firing', + actionGroups: [{ id: 'default', name: 'default' }], + defaultActionGroupId: 'default', + async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + const count = (state.count ?? 0) + 1; + + range(instances) + .map(() => ({ id: uuid.v4() })) + .forEach((instance: { id: string }) => { + services + .alertInstanceFactory(instance.id) + .replaceState({ triggerdOnCycle: count }) + .scheduleActions('default'); + }); + + return { + count, + }; + }, + producer: ALERTING_EXAMPLE_APP_ID, +}; diff --git a/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts similarity index 67% rename from examples/alerting_example/server/alert_types/astros.ts rename to x-pack/examples/alerting_example/server/alert_types/astros.ts index 1ccf8af4ce623b..852e6f57d11066 100644 --- a/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -1,24 +1,11 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import axios from 'axios'; -import { AlertType } from '../../../../x-pack/plugins/alerts/server'; +import { AlertType } from '../../../../plugins/alerts/server'; import { Operator, Craft, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; interface PeopleInSpace { diff --git a/x-pack/examples/alerting_example/server/index.ts b/x-pack/examples/alerting_example/server/index.ts new file mode 100644 index 00000000000000..95676bec735553 --- /dev/null +++ b/x-pack/examples/alerting_example/server/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. + */ + +import { PluginInitializer } from 'kibana/server'; +import { AlertingExamplePlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new AlertingExamplePlugin(); diff --git a/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts similarity index 64% rename from examples/alerting_example/server/plugin.ts rename to x-pack/examples/alerting_example/server/plugin.ts index 4141b48ffeeaf2..69971846e32b0f 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -1,31 +1,18 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import { Plugin, CoreSetup } from 'kibana/server'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_APP_CATEGORIES } from '../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { PluginSetupContract as AlertingSetup } from '../../../plugins/alerts/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; -import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server'; +import { INDEX_THRESHOLD_ID } from '../../../plugins/alerting_builtins/server'; import { ALERTING_EXAMPLE_APP_ID } from '../common/constants'; // this plugin's dependendencies diff --git a/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json similarity index 64% rename from examples/alerting_example/tsconfig.json rename to x-pack/examples/alerting_example/tsconfig.json index 214e4b78a9a701..95d42d40aceb3f 100644 --- a/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target" }, @@ -9,10 +9,10 @@ "public/**/*.tsx", "server/**/*.ts", "common/**/*.ts", - "../../typings/**/*", + "../../../typings/**/*", ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" } ] } diff --git a/x-pack/package.json b/x-pack/package.json index 984843e5b6360d..482e832aea9160 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -80,7 +80,7 @@ "@types/geojson": "7946.0.7", "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", @@ -107,7 +107,7 @@ "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", - "@types/pngjs": "^3.3.2", + "@types/pngjs": "^3.4.0", "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", @@ -139,12 +139,11 @@ "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "angular": "^1.8.0", - "angular-sanitize": "1.8.0", + "angular-sanitize": "^1.8.0", "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.7", "apollo-link-state": "^0.4.1", "autoprefixer": "^9.7.4", - "axios": "^0.19.0", "babel-jest": "^25.5.1", "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", @@ -156,7 +155,7 @@ "chalk": "^4.1.0", "chance": "1.0.18", "cheerio": "0.22.0", - "commander": "3.0.2", + "commander": "^3.0.2", "constate": "^1.3.2", "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", @@ -211,7 +210,7 @@ "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", "mochawesome-merge": "^4.1.0", - "mustache": "^2.3.0", + "mustache": "^2.3.2", "mutation-observer": "^1.0.3", "null-loader": "^3.0.0", "oboe": "^2.1.4", @@ -257,7 +256,6 @@ "supertest-as-promised": "^4.0.2", "suricata-sid-db": "^1.0.2", "tinycolor2": "1.4.1", - "tmp": "0.1.0", "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "ts-loader": "^6.0.4", @@ -298,8 +296,8 @@ "apollo-link-schema": "^1.1.0", "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", - "archiver": "3.1.1", - "axios": "^0.19.0", + "archiver": "^3.1.1", + "axios": "^0.19.2", "bluebird": "3.5.5", "boom": "^7.2.0", "chroma-js": "^1.4.1", @@ -328,7 +326,7 @@ "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", "handlebars": "4.7.6", - "history": "4.9.0", + "history": "^4.9.0", "idx": "^2.5.6", "immer": "^1.5.0", "inline-style": "^2.0.0", @@ -356,13 +354,13 @@ "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", - "pngjs": "3.4.0", + "pngjs": "^3.4.0", "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", "query-string": "^6.13.2", - "raw-loader": "3.1.0", + "raw-loader": "^3.1.0", "react": "^16.12.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", @@ -377,7 +375,7 @@ "request": "^2.88.0", "rison-node": "0.3.1", "rxjs": "^6.5.5", - "semver": "5.7.0", + "semver": "^5.7.0", "set-value": "^3.0.2", "squel": "^5.13.0", "stats-lite": "^2.2.0", diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index a6cffb02848159..d4817eab64acbe 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1696,14 +1696,22 @@ describe('muteAll()', () => { muteAll: false, }, references: [], + version: '123', }); await alertsClient.muteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - muteAll: true, - mutedInstanceIds: [], - updatedBy: 'elastic', - }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: true, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); }); describe('authorization', () => { @@ -1785,11 +1793,18 @@ describe('unmuteAll()', () => { }); await alertsClient.unmuteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - muteAll: false, - mutedInstanceIds: [], - updatedBy: 'elastic', - }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: false, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); }); describe('authorization', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 671b1d6411d7fc..033fdd752c6958 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -45,6 +45,8 @@ import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log'; import { IEvent } from '../../event_log/server'; import { parseDuration } from '../common/parse_duration'; +import { retryIfConflicts } from './lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from './saved_objects'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -421,6 +423,14 @@ export class AlertsClient { } public async update({ id, data }: UpdateOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.update('${id}')`, + async () => await this.updateWithOCC({ id, data }) + ); + } + + private async updateWithOCC({ id, data }: UpdateOptions): Promise { let alertSavedObject: SavedObject; try { @@ -529,7 +539,15 @@ export class AlertsClient { }; } - public async updateApiKey({ id }: { id: string }) { + public async updateApiKey({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.updateApiKey('${id}')`, + async () => await this.updateApiKeyWithOCC({ id }) + ); + } + + private async updateApiKeyWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -597,7 +615,15 @@ export class AlertsClient { } } - public async enable({ id }: { id: string }) { + public async enable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.enable('${id}')`, + async () => await this.enableWithOCC({ id }) + ); + } + + private async enableWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -658,7 +684,15 @@ export class AlertsClient { } } - public async disable({ id }: { id: string }) { + public async disable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.disable('${id}')`, + async () => await this.disableWithOCC({ id }) + ); + } + + private async disableWithOCC({ id }: { id: string }) { let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; let version: string | undefined; @@ -711,8 +745,19 @@ export class AlertsClient { } } - public async muteAll({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + public async muteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteAll('${id}')`, + async () => await this.muteAllWithOCC({ id }) + ); + } + + private async muteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -723,19 +768,34 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update( - 'alert', + const updateAttributes = this.updateMeta({ + muteAll: true, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, id, - this.updateMeta({ - muteAll: true, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }) + updateAttributes, + updateOptions + ); + } + + public async unmuteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteAll('${id}')`, + async () => await this.unmuteAllWithOCC({ id }) ); } - public async unmuteAll({ id }: { id: string }) { - const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + private async unmuteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -746,18 +806,30 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update( - 'alert', + const updateAttributes = this.updateMeta({ + muteAll: false, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, id, - this.updateMeta({ - muteAll: false, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }) + updateAttributes, + updateOptions + ); + } + + public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteInstance('${alertId}')`, + async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) ); } - public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { + private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId @@ -788,7 +860,15 @@ export class AlertsClient { } } - public async unmuteInstance({ + public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteInstance('${alertId}')`, + async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) + ); + } + + private async unmuteInstanceWithOCC({ alertId, alertInstanceId, }: { diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts new file mode 100644 index 00000000000000..1c5edb45c80fe3 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -0,0 +1,359 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; + +import { AlertsClient, ConstructorOptions } from './alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from './alert_type_registry.mock'; +import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../actions/server'; +import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import { RetryForConflictsAttempts } from './lib/retry_if_conflicts'; +import { TaskStatus } from '../../../plugins/task_manager/server/task'; + +let alertsClient: AlertsClient; + +const MockAlertId = 'alert-id'; + +const ConflictAfterRetries = RetryForConflictsAttempts + 1; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const logger = loggingSystemMock.create().get(); +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger, + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +// this suite consists of two suites running tests against mutable alertsClient APIs: +// - one to run tests where an SO update conflicts once +// - one to run tests where an SO update conflicts too many times +describe('alerts_client_conflict_retries', () => { + // tests that mutable operations work if only one SO conflict occurs + describe(`1 retry works for method`, () => { + beforeEach(() => { + mockSavedObjectUpdateConflictErrorTimes(1); + }); + + testFn(update, true); + testFn(updateApiKey, true); + testFn(enable, true); + testFn(disable, true); + testFn(muteAll, true); + testFn(unmuteAll, true); + testFn(muteInstance, true); + testFn(unmuteInstance, true); + }); + + // tests that mutable operations fail if too many SO conflicts occurs + describe(`${ConflictAfterRetries} retries fails with conflict error`, () => { + beforeEach(() => { + mockSavedObjectUpdateConflictErrorTimes(ConflictAfterRetries); + }); + + testFn(update, false); + testFn(updateApiKey, false); + testFn(enable, false); + testFn(disable, false); + testFn(muteAll, false); + testFn(unmuteAll, false); + testFn(muteInstance, false); + testFn(unmuteInstance, false); + }); +}); + +// alertsClients methods being tested +// - success is passed as an indication if the alertsClient method +// is expected to succeed or not, based on the number of conflicts +// set up in the `beforeEach()` method + +async function update(success: boolean) { + try { + await alertsClient.update({ + id: MockAlertId, + data: { + schedule: { interval: '5s' }, + name: 'cba', + tags: ['bar'], + params: { bar: true }, + throttle: '10s', + actions: [], + }, + }); + } catch (err) { + // only checking the warn messages in this test + expect(logger.warn).lastCalledWith( + `alertsClient.update('alert-id') conflict, exceeded retries` + ); + return expectConflict(success, err, 'create'); + } + expectSuccess(success, 2, 'create'); + + // only checking the debug messages in this test + expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`); +} + +async function updateApiKey(success: boolean) { + try { + await alertsClient.updateApiKey({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function enable(success: boolean) { + setupRawAlertMocks({}, { enabled: false }); + + try { + await alertsClient.enable({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + // a successful enable call makes 2 calls to update, so that's 3 total, + // 1 with conflict + 2 on success + expectSuccess(success, 3); +} + +async function disable(success: boolean) { + try { + await alertsClient.disable({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function muteAll(success: boolean) { + try { + await alertsClient.muteAll({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function unmuteAll(success: boolean) { + try { + await alertsClient.unmuteAll({ id: MockAlertId }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function muteInstance(success: boolean) { + try { + await alertsClient.muteInstance({ alertId: MockAlertId, alertInstanceId: 'instance-id' }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +async function unmuteInstance(success: boolean) { + setupRawAlertMocks({}, { mutedInstanceIds: ['instance-id'] }); + try { + await alertsClient.unmuteInstance({ alertId: MockAlertId, alertInstanceId: 'instance-id' }); + } catch (err) { + return expectConflict(success, err); + } + + expectSuccess(success); +} + +// tests to run when the method is expected to succeed +function expectSuccess( + success: boolean, + count: number = 2, + method: 'update' | 'create' = 'update' +) { + expect(success).toBe(true); + expect(unsecuredSavedObjectsClient[method]).toHaveBeenCalledTimes(count); + // message content checked in the update test + expect(logger.debug).toHaveBeenCalled(); +} + +// tests to run when the method is expected to fail +function expectConflict(success: boolean, err: Error, method: 'update' | 'create' = 'update') { + const conflictErrorMessage = SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + .message; + + expect(`${err}`).toBe(`Error: ${conflictErrorMessage}`); + expect(success).toBe(false); + expect(unsecuredSavedObjectsClient[method]).toHaveBeenCalledTimes(ConflictAfterRetries); + // message content checked in the update test + expect(logger.debug).toBeCalledTimes(RetryForConflictsAttempts); + expect(logger.warn).toBeCalledTimes(1); +} + +// wrapper to call the test function with a it's own name +function testFn(fn: (success: boolean) => unknown, success: boolean) { + test(`${fn.name}`, async () => await fn(success)); +} + +// set up mocks for update or create (the update() method uses create!) +function mockSavedObjectUpdateConflictErrorTimes(times: number) { + // default success value + const mockUpdateValue = { + id: MockAlertId, + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'scheduled-task-id', + }, + references: [], + }; + + unsecuredSavedObjectsClient.update.mockResolvedValue(mockUpdateValue); + unsecuredSavedObjectsClient.create.mockResolvedValue(mockUpdateValue); + + // queue up specified number of errors before a success call + for (let i = 0; i < times; i++) { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + ); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId) + ); + } +} + +// set up mocks needed to get the tested methods to run +function setupRawAlertMocks( + overrides: Record = {}, + attributeOverrides: Record = {} +) { + const rawAlert = { + id: MockAlertId, + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '10s' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: {}, + throttle: null, + actions: [], + muteAll: false, + mutedInstanceIds: [], + ...attributeOverrides, + }, + references: [], + version: '123', + ...overrides, + }; + const decryptedRawAlert = { + ...rawAlert, + attributes: { + ...rawAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + unsecuredSavedObjectsClient.get.mockReset(); + encryptedSavedObjects.getDecryptedAsInternalUser.mockReset(); + + // splitting this out as it's easier to set a breakpoint :-) + // eslint-disable-next-line prettier/prettier + unsecuredSavedObjectsClient.get.mockImplementation(async () => + cloneDeep(rawAlert) + ); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockImplementation(async () => + cloneDeep(decryptedRawAlert) + ); +} + +// setup for each test +beforeEach(() => { + jest.resetAllMocks(); + + alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); + alertsClientParams.invalidateAPIKey.mockResolvedValue({ + apiKeysEnabled: true, + result: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + }, + }); + alertsClientParams.getUserName.mockResolvedValue('elastic'); + + taskManager.runNow.mockResolvedValue({ id: '' }); + taskManager.schedule.mockResolvedValue({ + id: 'scheduled-task-id', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + ownerId: null, + taskType: 'task-type', + params: {}, + }); + + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockResolvedValue([]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + + alertTypeRegistry.get.mockImplementation((id) => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + })); + + alertTypeRegistry.get.mockReturnValue({ + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + + alertsClient = new AlertsClient(alertsClientParams); + + setupRawAlertMocks(); +}); diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.ts new file mode 100644 index 00000000000000..19caccc753e388 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.test.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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { retryIfConflicts, RetryForConflictsAttempts } from './retry_if_conflicts'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; + +describe('retry_if_conflicts', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should work when operation is a success', async () => { + const result = await retryIfConflicts(MockLogger, MockOperationName, OperationSuccessful); + expect(result).toBe(MockResult); + }); + + test('should throw error if not a conflict error', async () => { + await expect( + retryIfConflicts(MockLogger, MockOperationName, OperationFailure) + ).rejects.toThrowError('wops'); + }); + + for (let i = 1; i <= RetryForConflictsAttempts; i++) { + test(`should work when operation conflicts ${i} times`, async () => { + const result = await retryIfConflicts( + MockLogger, + MockOperationName, + getOperationConflictsTimes(i) + ); + expect(result).toBe(MockResult); + expect(MockLogger.debug).toBeCalledTimes(i); + for (let j = 0; j < i; j++) { + expect(MockLogger.debug).nthCalledWith(i, `${MockOperationName} conflict, retrying ...`); + } + }); + } + + test(`should throw conflict error when conflicts > ${RetryForConflictsAttempts} times`, async () => { + await expect( + retryIfConflicts( + MockLogger, + MockOperationName, + getOperationConflictsTimes(RetryForConflictsAttempts + 1) + ) + ).rejects.toThrowError(SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId)); + expect(MockLogger.debug).toBeCalledTimes(RetryForConflictsAttempts); + expect(MockLogger.warn).toBeCalledTimes(1); + expect(MockLogger.warn).toBeCalledWith(`${MockOperationName} conflict, exceeded retries`); + }); +}); + +const MockAlertId = 'alert-id'; +const MockOperationName = 'conflict-retryable-operation'; +const MockLogger = loggingSystemMock.create().get(); +const MockResult = 42; + +async function OperationSuccessful() { + return MockResult; +} + +async function OperationFailure() { + throw new Error('wops'); +} + +function getOperationConflictsTimes(times: number) { + return async function OperationConflictsTimes() { + times--; + if (times >= 0) { + throw SavedObjectsErrorHelpers.createConflictError('alert', MockAlertId); + } + + return MockResult; + }; +} diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts new file mode 100644 index 00000000000000..9cb1d7975855c2 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.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. + */ + +// This module provides a helper to perform retries on a function if the +// function ends up throwing a SavedObject 409 conflict. This can happen +// when alert SO's are updated in the background, and will avoid having to +// have the caller make explicit conflict checks, where the conflict was +// caused by a background update. + +import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +type RetryableForConflicts = () => Promise; + +// number of times to retry when conflicts occur +// note: it seems unlikely that we'd need more than one retry, but leaving +// this statically configurable in case we DO need > 1 +export const RetryForConflictsAttempts = 1; + +// milliseconds to wait before retrying when conflicts occur +// note: we considered making this random, to help avoid a stampede, but +// with 1 retry it probably doesn't matter, and adding randomness could +// make it harder to diagnose issues +const RetryForConflictsDelay = 250; + +// retry an operation if it runs into 409 Conflict's, up to a limit +export async function retryIfConflicts( + logger: Logger, + name: string, + operation: RetryableForConflicts, + retries: number = RetryForConflictsAttempts +): Promise { + // run the operation, return if no errors or throw if not a conflict error + try { + return await operation(); + } catch (err) { + if (!SavedObjectsErrorHelpers.isConflictError(err)) { + throw err; + } + + // must be a conflict; if no retries left, throw it + if (retries <= 0) { + logger.warn(`${name} conflict, exceeded retries`); + throw err; + } + + // delay a bit before retrying + logger.debug(`${name} conflict, retrying ...`); + await waitBeforeNextRetry(); + return await retryIfConflicts(logger, name, operation, retries - 1); + } +} + +async function waitBeforeNextRetry(): Promise { + await new Promise((resolve) => setTimeout(resolve, RetryForConflictsDelay)); +} diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 06ce8d673e6b71..51ac68b5899776 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -9,6 +9,23 @@ import mappings from './mappings.json'; import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +export { partiallyUpdateAlert } from './partially_update_alert'; + +export const AlertAttributesExcludedFromAAD = [ + 'scheduledTaskId', + 'muteAll', + 'mutedInstanceIds', + 'updatedBy', +]; + +// useful for Pick which is a +// type which is a subset of RawAlert with just attributes excluded from AAD +export type AlertAttributesExcludedFromAADType = + | 'scheduledTaskId' + | 'muteAll' + | 'mutedInstanceIds' + | 'updatedBy'; + export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts new file mode 100644 index 00000000000000..50815c797e399b --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObjectsClientContract, + ISavedObjectsRepository, + SavedObjectsErrorHelpers, +} from '../../../../../src/core/server'; + +import { partiallyUpdateAlert, PartiallyUpdateableAlertAttributes } from './partially_update_alert'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; + +const MockSavedObjectsClientContract = savedObjectsClientMock.create(); +const MockISavedObjectsRepository = (MockSavedObjectsClientContract as unknown) as jest.Mocked< + ISavedObjectsRepository +>; + +describe('partially_update_alert', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + for (const [soClientName, soClient] of Object.entries(getMockSavedObjectClients())) + describe(`using ${soClientName}`, () => { + test('should work with no options', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should work with extraneous attributes ', async () => { + const attributes = (InvalidAttributes as unknown) as PartiallyUpdateableAlertAttributes; + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, attributes); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should handle SO errors', async () => { + soClient.update.mockRejectedValueOnce(new Error('wops')); + + await expect( + partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes) + ).rejects.toThrowError('wops'); + }); + + test('should handle the version option', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { version: '1.2.3' }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, { + version: '1.2.3', + }); + }); + + test('should handle the ignore404 option', async () => { + const err = SavedObjectsErrorHelpers.createGenericNotFoundError(); + soClient.update.mockRejectedValueOnce(err); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { ignore404: true }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, {}); + }); + + test('should handle the namespace option', async () => { + soClient.update.mockResolvedValueOnce(MockUpdateValue); + + await partiallyUpdateAlert(soClient, MockAlertId, DefaultAttributes, { + namespace: 'bat.cave', + }); + expect(soClient.update).toHaveBeenCalledWith('alert', MockAlertId, DefaultAttributes, { + namespace: 'bat.cave', + }); + }); + }); +}); + +function getMockSavedObjectClients(): Record< + string, + jest.Mocked +> { + return { + SavedObjectsClientContract: MockSavedObjectsClientContract, + // doesn't appear to be a mock for this, but it's basically the same as the above, + // so just cast it to make sure we catch any type errors + ISavedObjectsRepository: MockISavedObjectsRepository, + }; +} + +const DefaultAttributes = { + scheduledTaskId: 'scheduled-task-id', + muteAll: true, + mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'], + updatedBy: 'someone', +}; + +const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' }; + +const MockAlertId = 'alert-id'; + +const MockUpdateValue = { + id: MockAlertId, + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'scheduled-task-id', + }, + references: [], +}; diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.ts new file mode 100644 index 00000000000000..cc25aaba357981 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.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 { pick } from 'lodash'; +import { RawAlert } from '../types'; + +import { + SavedObjectsClient, + SavedObjectsErrorHelpers, + SavedObjectsUpdateOptions, +} from '../../../../../src/core/server'; + +import { AlertAttributesExcludedFromAAD, AlertAttributesExcludedFromAADType } from './index'; + +export type PartiallyUpdateableAlertAttributes = Pick; + +export interface PartiallyUpdateAlertSavedObjectOptions { + version?: string; + ignore404?: boolean; + namespace?: string; // only should be used with ISavedObjectsRepository +} + +// typed this way so we can send a SavedObjectClient or SavedObjectRepository +type SavedObjectClientForUpdate = Pick; + +// direct, partial update to an alert saved object via scoped SavedObjectsClient +// using namespace set in the client +export async function partiallyUpdateAlert( + savedObjectsClient: SavedObjectClientForUpdate, + id: string, + attributes: PartiallyUpdateableAlertAttributes, + options: PartiallyUpdateAlertSavedObjectOptions = {} +): Promise { + // ensure we only have the valid attributes excluded from AAD + const attributeUpdates = pick(attributes, AlertAttributesExcludedFromAAD); + const updateOptions: SavedObjectsUpdateOptions = pick(options, 'namespace', 'version'); + + try { + await savedObjectsClient.update('alert', id, attributeUpdates, updateOptions); + } catch (err) { + if (options?.ignore404 && SavedObjectsErrorHelpers.isNotFoundError(err)) { + return; + } + throw err; + } +} diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 56204b5fb9f9d7..649198b7eae11a 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -7,23 +7,21 @@ "cypress:open": "cypress open", "cypress:run": "cypress run --spec **/*.feature" }, - "dependencies": { + "devDependencies": { "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^5.4.1", + "@types/cypress-cucumber-preprocessor": "^1.14.1", + "@types/node": ">=10.17.17 <10.20.0", "axios": "^0.19.2", - "cypress": "^4.9.0", "cypress-cucumber-preprocessor": "^2.5.2", + "cypress": "^4.9.0", "ora": "^4.0.4", "p-limit": "^3.0.1", "p-retry": "^4.2.0", "ts-loader": "^7.0.5", "typescript": "4.0.2", "wait-on": "^5.0.1", - "webpack": "^4.43.0", - "yargs": "^15.4.0" - }, - "devDependencies": { - "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/node": "^14.0.14" + "webpack": "^4.41.5", + "yargs": "^15.4.1" } } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx index 22d50ca0d5c41a..6107a8e764adb1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx @@ -15,11 +15,11 @@ import { i18n } from '@kbn/i18n'; import { PaletteLegends } from './PaletteLegends'; import { ColorPaletteFlexItem } from './ColorPaletteFlexItem'; import { - AVERAGE_LABEL, - GOOD_LABEL, + CV_AVERAGE_LABEL, + CV_GOOD_LABEL, LESS_LABEL, MORE_LABEL, - POOR_LABEL, + CV_POOR_LABEL, } from './translations'; export interface Thresholds { @@ -51,7 +51,7 @@ export function getCoreVitalTooltipMessage( values: { percentage, title: title?.toLowerCase(), - exp: good ? GOOD_LABEL : bad ? POOR_LABEL : AVERAGE_LABEL, + exp: good ? CV_GOOD_LABEL : bad ? CV_POOR_LABEL : CV_AVERAGE_LABEL, moreOrLess: bad || average ? MORE_LABEL : LESS_LABEL, value: good || average ? thresholds.good : thresholds.bad, averageMessage: average @@ -90,7 +90,7 @@ export function CoreVitalItem({ {palette.map((hexCode, ind) => ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx index 84cc5f1ddb230b..43eb468b883173 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx @@ -10,16 +10,36 @@ import { EuiFlexItem, EuiHealth, euiPaletteForStatus, + EuiText, EuiToolTip, } from '@elastic/eui'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { getCoreVitalTooltipMessage, Thresholds } from './CoreVitalItem'; +import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + LEGEND_NEEDS_IMPROVEMENT_LABEL, + LEGEND_GOOD_LABEL, + LEGEND_POOR_LABEL, +} from './translations'; const PaletteLegend = styled(EuiHealth)` &:hover { cursor: pointer; text-decoration: underline; - background-color: #e7f0f7; + } +`; + +const StyledSpan = styled.span<{ + darkMode: boolean; +}>` + &:hover { + background-color: ${(props) => + props.darkMode + ? euiDarkVars.euiColorLightestShade + : euiLightVars.euiColorLightestShade}; } `; @@ -36,10 +56,17 @@ export function PaletteLegends({ onItemHover, thresholds, }: Props) { + const [darkMode] = useUiSetting$('theme:darkMode'); + const palette = euiPaletteForStatus(3); + const labels = [ + LEGEND_GOOD_LABEL, + LEGEND_NEEDS_IMPROVEMENT_LABEL, + LEGEND_POOR_LABEL, + ]; return ( - + {palette.map((color, ind) => ( - {ranks?.[ind]}% + + + + {labels[ind]} ({ranks?.[ind]}%) + + + + ))} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx index 0135f0b3695377..cd7fd0af6d6838 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { CLS_LABEL, FID_LABEL, LCP_LABEL } from './translations'; import { CoreVitalItem } from './CoreVitalItem'; import { UXMetrics } from '../UXMetrics'; +import { formatToSec } from '../UXMetrics/KeyUXMetrics'; const CoreVitalsThresholds = { LCP: { good: '2.5s', bad: '4.0s' }, @@ -28,7 +29,7 @@ export function CoreVitals({ data, loading }: Props) { setIsPopoverOpen(false); + return ( @@ -72,7 +81,37 @@ export function UXMetrics() { -

{I18LABELS.coreWebVitals}

+

+ {I18LABELS.coreWebVitals} + setIsPopoverOpen(true)} + color={'text'} + iconType={'questionInCircle'} + /> + } + closePopover={closePopover} + > +
+ + + + {' '} + {I18LABELS.coreWebVitals} + + +
+
+

diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx new file mode 100644 index 00000000000000..128b5696b23b88 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_prompt.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; + +export function EmptyPrompt() { + return ( + + {i18n.translate('xpack.apm.serviceMap.noServicesPromptTitle', { + defaultMessage: 'No services available', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.serviceMap.noServicesPromptDescription', { + defaultMessage: + 'We can’t find any services to map within the currently selected time range and environment. Please try another range or check the environment selected. If you don’t have any services, please use our setup instructions to get started.', + })} +

+ } + actions={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index f504ac1f68ce19..2a5b4ce44ff467 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -9,16 +9,30 @@ import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; import { LicenseContext } from '../../../context/LicenseContext'; +import * as useFetcherModule from '../../../hooks/useFetcher'; import { ServiceMap } from './'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const activeLicense = new License({ + signature: 'active test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'active', + type: 'platinum', + uid: '1', + }, +}); + const expiredLicense = new License({ - signature: 'test signature', + signature: 'expired test signature', license: { expiryDateInMillis: 0, mode: 'platinum', @@ -28,26 +42,58 @@ const expiredLicense = new License({ }, }); -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - - {children} - - - ); +function createWrapper(license: License | null) { + return ({ children }: { children?: ReactNode }) => { + return ( + + + + + {children} + + + + + ); + }; } describe('ServiceMap', () => { - describe('with an inactive license', () => { + describe('with no license', () => { + it('renders null', async () => { + expect( + await render(, { + wrapper: createWrapper(null), + }).queryByTestId('ServiceMap') + ).not.toBeInTheDocument(); + }); + }); + + describe('with an expired license', () => { it('renders the license banner', async () => { expect( - ( + await render(, { + wrapper: createWrapper(expiredLicense), + }).findAllByText(/Platinum/) + ).toHaveLength(1); + }); + }); + + describe('with an active license', () => { + describe('with an empty response', () => { + it('renders the empty banner', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ + data: { elements: [] }, + refetch: () => {}, + status: FETCH_STATUS.SUCCESS, + }); + + expect( await render(, { - wrapper: Wrapper, - }).findAllByText(/Platinum/) - ).length - ).toBeGreaterThan(0); + wrapper: createWrapper(activeLicense), + }).findAllByText(/No services available/) + ).toHaveLength(1); + }); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index bb450131bdfb88..1d2e4ada43add8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React, { ReactNode } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -21,6 +21,7 @@ import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; +import { EmptyPrompt } from './empty_prompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; @@ -28,12 +29,39 @@ interface ServiceMapProps { serviceName?: string; } +function PromptContainer({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +function LoadingSpinner() { + return ( + + ); +} + export function ServiceMap({ serviceName }: ServiceMapProps) { const theme = useTheme(); const license = useLicense(); const { urlParams } = useUrlParams(); - const { data = { elements: [] } } = useFetcher(() => { + const { data = { elements: [] }, status } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. if (!license || !isActivePlatinumLicense(license)) { return; @@ -65,37 +93,41 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isActivePlatinumLicense(license) ? ( + if (!isActivePlatinumLicense(license)) { + return ( + + + + ); + } + + if (status === FETCH_STATUS.SUCCESS && data.elements.length === 0) { + return ( + + + + ); + } + + return (
{serviceName && } + {status === FETCH_STATUS.LOADING && }
- ) : ( - - - - - ); } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts index d944e3fa6db7c7..1586e1f4903a2d 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts @@ -5,8 +5,6 @@ */ import { getFormattedBuckets } from '../index'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../server/lib/transactions/distribution/get_buckets/transform'; describe('Distribution', () => { it('getFormattedBuckets', () => { @@ -20,6 +18,7 @@ describe('Distribution', () => { samples: [ { transactionId: 'someTransactionId', + traceId: 'someTraceId', }, ], }, @@ -29,10 +28,12 @@ describe('Distribution', () => { samples: [ { transactionId: 'anotherTransactionId', + traceId: 'anotherTraceId', }, ], }, - ] as IBucket[]; + ]; + expect(getFormattedBuckets(buckets, 20)).toEqual([ { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index fa5a9956c82874..e18e6b1ed914e8 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -13,7 +13,7 @@ import { ValuesType } from 'utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-expect-error @@ -30,7 +30,10 @@ interface IChartPoint { }; } -export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) { +export function getFormattedBuckets( + buckets: DistributionBucket[], + bucketSize: number +) { if (!buckets) { return []; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 0845e2c9b14120..3bb23fd6396ca3 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -18,7 +18,7 @@ import { Location } from 'history'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -34,7 +34,7 @@ interface Props { waterfall: IWaterfall; exceedsMax: boolean; isLoading: boolean; - traceSamples: IBucket['samples']; + traceSamples: DistributionBucket['samples']; } export function WaterfallWithSummmary({ diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 65482c9d21c16d..cd3e02d1556029 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, omit } from 'lodash'; +import { flatten, omit, isEmpty } from 'lodash'; import { useHistory, useParams } from 'react-router-dom'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; @@ -69,11 +69,11 @@ export function useTransactionDistribution(urlParams: IUrlParams) { // selected sample was not found. select a new one: // sorted by total number of requests, but only pick // from buckets that have samples - const preferredSample = maybe( - response.buckets - .filter((bucket) => bucket.samples.length > 0) - .sort((bucket) => bucket.count)[0]?.samples[0] - ); + const bucketsSortedByCount = response.buckets + .filter((bucket) => !isEmpty(bucket.samples)) + .sort((bucket) => bucket.count); + + const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]); history.push({ ...history.location, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index 812cf9865bda83..bd4bdb9ca3536d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -81,6 +81,7 @@ export async function getLongTaskMetrics({ } } }); + return { noOfLongTasks, sumOfLongTasks, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index fa34c2e25fecd9..0828d7ab651906 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -124,13 +124,12 @@ export async function getWebCoreVitals({ { value: 0, key: 0 }, ]; - // Divide by 1000 to convert ms into seconds return { cls: String(cls?.values['50.0']?.toFixed(2) || 0), - fid: ((fid?.values['50.0'] || 0) / 1000).toFixed(2), - lcp: ((lcp?.values['50.0'] || 0) / 1000).toFixed(2), - tbt: tbt?.values['50.0'] || 0, - fcp: fcp?.values['50.0'] || 0, + fid: fid?.values['50.0'] ?? 0, + lcp: lcp?.values['50.0'] ?? 0, + tbt: tbt?.values['50.0'] ?? 0, + fcp: fcp?.values['50.0'] ?? 0, lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks), fidRanks: getRanksPercentages(fidRanks?.values ?? defaultRanks), diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index e532265de24ece..c63dfcc0c0ec70 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -639,7 +639,7 @@ Object { "body": Object { "aggs": Object { "stats": Object { - "extended_stats": Object { + "max": Object { "field": "transaction.duration.us", }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts deleted file mode 100644 index bfe72bf7c00f93..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ProcessorEvent } from '../../../../../common/processor_event'; -import { - SERVICE_NAME, - TRACE_ID, - TRANSACTION_DURATION, - TRANSACTION_ID, - TRANSACTION_NAME, - TRANSACTION_SAMPLED, - TRANSACTION_TYPE, -} from '../../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../../common/utils/range_filter'; -import { - Setup, - SetupTimeRange, - SetupUIFilters, -} from '../../../helpers/setup_request'; - -export async function bucketFetcher( - serviceName: string, - transactionName: string, - transactionType: string, - transactionId: string, - traceId: string, - distributionMax: number, - bucketSize: number, - setup: Setup & SetupTimeRange & SetupUIFilters -) { - const { start, end, uiFiltersES, apmEventClient } = setup; - - const params = { - apm: { - events: [ProcessorEvent.transaction as const], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - should: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - ], - }, - }, - aggs: { - distribution: { - histogram: { - field: TRANSACTION_DURATION, - interval: bucketSize, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: distributionMax, - }, - }, - aggs: { - samples: { - filter: { - term: { [TRANSACTION_SAMPLED]: true }, - }, - aggs: { - items: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], - size: 10, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const response = await apmEventClient.search(params); - - return response; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index f3788b5d812a83..6e2fe34a5f5ef5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -3,35 +3,204 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { ValuesType } from 'utility-types'; +import { PromiseReturnType } from '../../../../../typings/common'; +import { joinByKey } from '../../../../../common/utils/join_by_key'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + SERVICE_NAME, + TRACE_ID, + TRANSACTION_DURATION, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../../common/utils/range_filter'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../../../helpers/setup_request'; -import { bucketFetcher } from './fetcher'; -import { bucketTransformer } from './transform'; - -export async function getBuckets( - serviceName: string, - transactionName: string, - transactionType: string, - transactionId: string, - traceId: string, - distributionMax: number, - bucketSize: number, - setup: Setup & SetupTimeRange & SetupUIFilters -) { - const response = await bucketFetcher( - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup - ); +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../../helpers/aggregated_transactions'; + +function getHistogramAggOptions({ + bucketSize, + field, + distributionMax, +}: { + bucketSize: number; + field: string; + distributionMax: number; +}) { + return { + field, + interval: bucketSize, + min_doc_count: 0, + extended_bounds: { + min: 0, + max: distributionMax, + }, + }; +} + +export async function getBuckets({ + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + distributionMax, + bucketSize, + setup, + searchAggregatedTransactions, +}: { + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + distributionMax: number; + bucketSize: number; + setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; +}) { + const { start, end, uiFiltersES, apmEventClient } = setup; + + const commonFilters = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES, + ]; - return bucketTransformer(response); + async function getSamplesForDistributionBuckets() { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + { term: { [TRANSACTION_SAMPLED]: true } }, + ], + should: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [TRANSACTION_ID]: transactionId } }, + ], + }, + }, + aggs: { + distribution: { + histogram: getHistogramAggOptions({ + bucketSize, + field: TRANSACTION_DURATION, + distributionMax, + }), + aggs: { + samples: { + top_hits: { + _source: [TRANSACTION_ID, TRACE_ID], + size: 10, + sort: { + _score: 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.distribution.buckets.map((bucket) => { + return { + key: bucket.key, + samples: bucket.samples.hits.hits.map((hit) => ({ + traceId: hit._source.trace.id, + transactionId: hit._source.transaction.id, + })), + }; + }) ?? [] + ); + } + + async function getDistributionBuckets() { + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + }, + aggs: { + distribution: { + histogram: getHistogramAggOptions({ + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + bucketSize, + distributionMax, + }), + }, + }, + }, + }); + + return ( + response.aggregations?.distribution.buckets.map((bucket) => { + return { + key: bucket.key, + count: bucket.doc_count, + }; + }) ?? [] + ); + } + + const [ + samplesForDistributionBuckets, + distributionBuckets, + ] = await Promise.all([ + getSamplesForDistributionBuckets(), + getDistributionBuckets(), + ]); + + const buckets = joinByKey( + [...samplesForDistributionBuckets, ...distributionBuckets], + 'key' + ).map((bucket) => ({ + ...bucket, + samples: bucket.samples ?? [], + count: bucket.count ?? 0, + })); + + return { + noHits: buckets.length === 0, + bucketSize, + buckets, + }; } + +export type DistributionBucket = ValuesType< + PromiseReturnType['buckets'] +>; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts deleted file mode 100644 index aee93e34f93a31..00000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { bucketFetcher } from './fetcher'; - -type DistributionBucketResponse = PromiseReturnType; - -export type IBucket = ReturnType; - -function getBucket( - bucket: Required< - DistributionBucketResponse - >['aggregations']['distribution']['buckets'][0] -) { - const samples = bucket.samples.items.hits.hits.map( - ({ _source }: { _source: Transaction }) => ({ - traceId: _source.trace.id, - transactionId: _source.transaction.id, - }) - ); - - return { - key: bucket.key, - count: bucket.doc_count, - samples, - }; -} - -export function bucketTransformer(response: DistributionBucketResponse) { - const buckets = - response.aggregations?.distribution.buckets.map(getBucket) || []; - - return { - noHits: response.hits.total.value === 0, - buckets, - }; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 139dac3df11718..24ca2a4a07b68c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, - TRANSACTION_DURATION, TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; @@ -16,18 +14,33 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; -export async function getDistributionMax( - serviceName: string, - transactionName: string, - transactionType: string, - setup: Setup & SetupTimeRange & SetupUIFilters -) { +export async function getDistributionMax({ + serviceName, + transactionName, + transactionType, + setup, + searchAggregatedTransactions, +}: { + serviceName: string; + transactionName: string; + transactionType: string; + setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; +}) { const { start, end, uiFiltersES, apmEventClient } = setup; const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -52,8 +65,10 @@ export async function getDistributionMax( }, aggs: { stats: { - extended_stats: { - field: TRANSACTION_DURATION, + max: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }, @@ -61,5 +76,5 @@ export async function getDistributionMax( }; const resp = await apmEventClient.search(params); - return resp.aggregations ? resp.aggregations.stats.max : null; + return resp.aggregations?.stats.value ?? null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts index 7c02852f83768e..b9ab36fb08d422 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts @@ -32,6 +32,7 @@ export async function getTransactionDistribution({ transactionId, traceId, setup, + searchAggregatedTransactions, }: { serviceName: string; transactionName: string; @@ -39,20 +40,23 @@ export async function getTransactionDistribution({ transactionId: string; traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { - const distributionMax = await getDistributionMax( + const distributionMax = await getDistributionMax({ serviceName, transactionName, transactionType, - setup - ); + setup, + searchAggregatedTransactions, + }); if (distributionMax == null) { return { noHits: true, buckets: [], bucketSize: 0 }; } const bucketSize = getBucketSize(distributionMax); - const { buckets, noHits } = await getBuckets( + + const { buckets, noHits } = await getBuckets({ serviceName, transactionName, transactionType, @@ -60,8 +64,9 @@ export async function getTransactionDistribution({ traceId, distributionMax, bucketSize, - setup - ); + setup, + searchAggregatedTransactions, + }); return { noHits, diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index 1c1d30c2d4d6d7..87b8bc7c4ae90f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -102,6 +102,7 @@ describe('transaction queries', () => { traceId: 'qux', transactionId: 'quz', setup, + searchAggregatedTransactions: false, }) ); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 10e917f385e719..dd1335fb2c2a19 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -124,6 +124,10 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({ traceId = '', } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionDistribution({ serviceName, transactionType, @@ -131,6 +135,7 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({ transactionId, traceId, setup, + searchAggregatedTransactions, }); }, })); diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index eb650ca5ad1524..39a8262a5deec1 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -5,11 +5,16 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { LegacyAPICaller } from 'kibana/server'; import { TelemetryCollector } from '../../types'; -import { workpadCollector } from './workpad_collector'; -import { customElementCollector } from './custom_element_collector'; +import { workpadCollector, workpadSchema, WorkpadTelemetry } from './workpad_collector'; +import { + customElementCollector, + CustomElementTelemetry, + customElementSchema, +} from './custom_element_collector'; + +type CanvasUsage = WorkpadTelemetry & CustomElementTelemetry; const collectors: TelemetryCollector[] = [workpadCollector, customElementCollector]; @@ -29,18 +34,19 @@ export function registerCanvasUsageCollector( return; } - const canvasCollector = usageCollection.makeUsageCollector({ + const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async (callCluster) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); return collectorResults.reduce((reduction, usage) => { return { ...reduction, ...usage }; - }, {}); + }, {}) as CanvasUsage; // We need the casting because `TelemetryCollector` claims it returns `Record` }, + schema: { ...workpadSchema, ...customElementSchema }, }); usageCollection.registerCollector(canvasCollector); diff --git a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts index 7b39e8b83b0457..d3ed1e17785ee9 100644 --- a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts @@ -6,6 +6,7 @@ import { SearchParams } from 'elasticsearch'; import { get } from 'lodash'; +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { collectFns } from './collector_helpers'; import { TelemetryCollector, @@ -19,7 +20,7 @@ interface CustomElementSearch { [CUSTOM_ELEMENT_TYPE]: TelemetryCustomElementDocument; } -interface CustomElementTelemetry { +export interface CustomElementTelemetry { custom_elements?: { count: number; elements: { @@ -31,6 +32,18 @@ interface CustomElementTelemetry { }; } +export const customElementSchema: MakeSchemaFrom = { + custom_elements: { + count: { type: 'long' }, + elements: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + functions_in_use: { type: 'array', items: { type: 'keyword' } }, + }, +}; + function isCustomElement(maybeCustomElement: any): maybeCustomElement is TelemetryCustomElement { return ( maybeCustomElement !== null && diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 9fa39c580962de..04794115288022 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -6,6 +6,7 @@ import { SearchParams } from 'elasticsearch'; import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; import { collectFns } from './collector_helpers'; import { TelemetryCollector, CanvasWorkpad } from '../../types'; @@ -15,7 +16,7 @@ interface WorkpadSearch { [CANVAS_TYPE]: CanvasWorkpad; } -interface WorkpadTelemetry { +export interface WorkpadTelemetry { workpads?: { total: number; }; @@ -54,6 +55,43 @@ interface WorkpadTelemetry { }; } +export const workpadSchema: MakeSchemaFrom = { + workpads: { total: { type: 'long' } }, + pages: { + total: { type: 'long' }, + per_workpad: { + avg: { type: 'float' }, + min: { type: 'long' }, + max: { type: 'long' }, + }, + }, + elements: { + total: { type: 'long' }, + per_page: { + avg: { type: 'float' }, + min: { type: 'long' }, + max: { type: 'long' }, + }, + }, + functions: { + total: { type: 'long' }, + in_use: { type: 'array', items: { type: 'keyword' } }, + per_element: { + avg: { type: 'float' }, + min: { type: 'long' }, + max: { type: 'long' }, + }, + }, + variables: { + total: { type: 'long' }, + per_workpad: { + avg: { type: 'float' }, + min: { type: 'long' }, + max: { type: 'long' }, + }, + }, +}; + /** Gather statistic about the given workpads @param workpadDocs a collection of workpad documents diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index 88a900f69c5ecf..f48f5fb91e3e70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -5,17 +5,13 @@ */ export { mockHistory, mockLocation } from './react_router_history.mock'; -export { mockKibanaContext } from './kibana_context.mock'; +export { mockKibanaValues } from './kibana_logic.mock'; export { mockLicensingValues } from './licensing_logic.mock'; export { mockHttpValues } from './http_logic.mock'; export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export { mockAllValues, mockAllActions, setMockValues } from './kea.mock'; -export { - mountWithContext, - mountWithKibanaContext, - mountWithAsyncContext, -} from './mount_with_context.mock'; +export { mountAsync } from './mount_async.mock'; +export { mountWithIntl } from './mount_with_i18n.mock'; export { shallowWithIntl } from './shallow_with_i18n.mock'; - -// Note: shallow_usecontext must be imported directly as a file +// Note: shallow_useeffect must be imported directly as a file diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index bad6beaa1652ee..b616cbab03e28e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -10,11 +10,13 @@ * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ +import { mockKibanaValues } from './kibana_logic.mock'; import { mockLicensingValues } from './licensing_logic.mock'; import { mockHttpValues } from './http_logic.mock'; import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { + ...mockKibanaValues, ...mockLicensingValues, ...mockHttpValues, ...mockFlashMessagesValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts similarity index 65% rename from x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index ee77b0937cd82a..9f3c2443bc9b8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * A set of default Kibana context values to use across component tests. - * @see enterprise_search/public/index.tsx for the KibanaContext definition/import - */ -export const mockKibanaContext = { +export const mockKibanaValues = { + config: { host: 'http://localhost:3002' }, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), - config: { host: 'http://localhost:3002' }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx new file mode 100644 index 00000000000000..a33e116c7ca726 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount, ReactWrapper } from 'enzyme'; + +import { mountWithIntl } from './'; + +/** + * This helper is intended for components that have async effects + * (e.g. http fetches) on mount. It mostly adds act/update boilerplate + * that's needed for the wrapper to play nice with Enzyme/Jest + * + * Example usage: + * + * const wrapper = mountAsync(); + */ + +interface IOptions { + i18n?: boolean; +} + +export const mountAsync = async ( + children: React.ReactElement, + options: IOptions +): Promise => { + let wrapper: ReactWrapper | undefined; + + // We get a lot of act() warning/errors in the terminal without this. + // TBH, I don't fully understand why since Enzyme's mount is supposed to + // have act() baked in - could be because of the wrapping context provider? + await act(async () => { + wrapper = options.i18n ? mountWithIntl(children) : mount(children); + }); + if (wrapper) { + wrapper.update(); // This seems to be required for the DOM to actually update + + return wrapper; + } else { + throw new Error('Could not mount wrapper'); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx deleted file mode 100644 index 646c3104c286fa..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount, ReactWrapper } from 'enzyme'; - -import { Provider } from 'react-redux'; -import { Store } from 'redux'; -import { getContext, resetContext } from 'kea'; - -import { I18nProvider } from '@kbn/i18n/react'; -import { KibanaContext } from '../'; -import { mockKibanaContext } from './kibana_context.mock'; - -/** - * This helper mounts a component with all the contexts/providers used - * by the production app, while allowing custom context to be - * passed in via a second arg - * - * Example usage: - * - * const wrapper = mountWithContext(, { config: { host: 'someOverride' } }); - */ -export const mountWithContext = (children: React.ReactNode, context?: object) => { - resetContext({ createStore: true }); - const store = getContext().store as Store; - - return mount( - - - {children} - - - ); -}; - -/** - * This helper mounts a component with just the default KibanaContext - - * useful for isolated / helper components that only need this context - * - * Same usage/override functionality as mountWithContext - */ -export const mountWithKibanaContext = (children: React.ReactNode, context?: object) => { - return mount( - - {children} - - ); -}; - -/** - * This helper is intended for components that have async effects - * (e.g. http fetches) on mount. It mostly adds act/update boilerplate - * that's needed for the wrapper to play nice with Enzyme/Jest - * - * Example usage: - * - * const wrapper = mountWithAsyncContext(, { http: { get: () => someData } }); - */ -export const mountWithAsyncContext = async ( - children: React.ReactNode, - context?: object -): Promise => { - let wrapper: ReactWrapper | undefined; - - // We get a lot of act() warning/errors in the terminal without this. - // TBH, I don't fully understand why since Enzyme's mount is supposed to - // have act() baked in - could be because of the wrapping context provider? - await act(async () => { - wrapper = mountWithContext(children, context); - }); - if (wrapper) { - wrapper.update(); // This seems to be required for the DOM to actually update - - return wrapper; - } else { - throw new Error('Could not mount wrapper'); - } -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx new file mode 100644 index 00000000000000..55abe1030544fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; + +/** + * This helper wraps a component with react-intl's which + * fixes "Could not find required `intl` object" console errors when running tests + * + * Example usage (should be the same as mount()): + * + * const wrapper = mountWithI18n(); + */ +export const mountWithIntl = (children: React.ReactElement) => { + return mount({children}); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts deleted file mode 100644 index df9e58994e36b4..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.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. - */ - -/** - * NOTE: These variable names MUST start with 'mock*' in order for - * Jest to accept its use within a jest.mock() - */ -import { mockKibanaContext } from './kibana_context.mock'; - -jest.mock('react', () => ({ - ...(jest.requireActual('react') as object), - useContext: jest.fn(() => ({ ...mockKibanaContext })), - useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior -})); - -/** - * Example usage within a component test using shallow(): - * - * import '../../../__mocks__/shallow_usecontext'; // Must come before React's import, adjust relative path as needed - * - * import React from 'react'; - * import { shallow } from 'enzyme'; - * - * // ... etc. - */ - -/** - * If you need to override the default mock context values, you can do so via jest.mockImplementation: - * - * import React, { useContext } from 'react'; - * - * // ... etc. - * - * it('some test', () => { - * useContext.mockImplementationOnce(() => ({ config: { host: 'someOverride' } })); - * }); - */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts new file mode 100644 index 00000000000000..732786b5f92495 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.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. + */ + +jest.mock('react', () => ({ + ...(jest.requireActual('react') as object), + useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior +})); + +/** + * Example usage within a component test using shallow(): + * + * import '../../../__mocks__/shallow_useeffect.mock'; // Must come before React's import, adjust relative path as needed + * + * import React from 'react'; + * import { shallow } from 'enzyme'; + * + * // ... etc. + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index 44afce96c1a6c0..f87ea2d4227803 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, ReactWrapper } from 'enzyme'; -import { mountWithAsyncContext, mockHttpValues, setMockValues } from '../../../__mocks__'; +import { mountAsync, mockHttpValues, setMockValues } from '../../../__mocks__'; import { LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -36,7 +36,7 @@ describe('EngineOverview', () => { }), }, }); - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EmptyState)).toHaveLength(1); }); @@ -69,7 +69,7 @@ describe('EngineOverview', () => { }); it('renders and calls the engines API', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EngineTable)).toHaveLength(1); expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { @@ -86,7 +86,7 @@ describe('EngineOverview', () => { hasPlatinumLicense: true, http: { ...mockHttpValues.http, get: mockApi }, }); - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); expect(wrapper.find(EngineTable)).toHaveLength(2); expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { @@ -103,7 +103,7 @@ describe('EngineOverview', () => { wrapper.find(EngineTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -111,7 +111,7 @@ describe('EngineOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountWithAsyncContext(); + const wrapper = await mountAsync(, { i18n: true }); await act(async () => getTablePagination(wrapper).onPaginate(5)); wrapper.update(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index c66fd24fee12a8..4d97a16991b715 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -6,11 +6,9 @@ import '../../../__mocks__/kea.mock'; import '../../../__mocks__/enterprise_search_url.mock'; -import { mockHttpValues } from '../../../__mocks__/'; +import { mockHttpValues, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n/react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); @@ -21,24 +19,22 @@ import { EngineTable } from './engine_table'; describe('EngineTable', () => { const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream - const wrapper = mount( - - - + const wrapper = mountWithIntl( + ); const table = wrapper.find(EuiBasicTable); @@ -78,13 +74,8 @@ describe('EngineTable', () => { }); it('handles empty data', () => { - const emptyWrapper = mount( - - {} }} - /> - + const emptyWrapper = mountWithIntl( + {} }} /> ); const emptyTable = emptyWrapper.find(EuiBasicTable); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 052f4446e4409b..c54d6ed3ddd6f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../__mocks__/shallow_usecontext.mock'; +import '../__mocks__/shallow_useeffect.mock'; import '../__mocks__/kea.mock'; import '../__mocks__/enterprise_search_url.mock'; -import React, { useContext } from 'react'; +import React from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues, useActions } from 'kea'; @@ -21,14 +21,14 @@ import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } f describe('AppSearch', () => { it('renders AppSearchUnconfigured when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(AppSearchUnconfigured)).toHaveLength(1); }); it('renders AppSearchConfigured when config.host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); expect(wrapper.find(AppSearchConfigured)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 410f6eb524822c..9aa2cce9c74df2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { KibanaContext, IKibanaContext } from '../index'; import { getAppSearchUrl } from '../shared/enterprise_search_url'; +import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; import { IInitialAppData } from '../../../common/types'; @@ -34,7 +34,7 @@ import { NotFound } from '../shared/not_found'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = (props) => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return !config.host ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index 35301af44b413d..b2030ec910cd87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -5,9 +5,9 @@ */ import '../../../__mocks__/kea.mock'; -import '../../../__mocks__/shallow_usecontext.mock'; -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; @@ -27,7 +27,6 @@ describe('ProductCard', () => { }); it('renders an App Search card', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -43,7 +42,6 @@ describe('ProductCard', () => { }); it('renders a Workplace Search card', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -61,7 +59,7 @@ describe('ProductCard', () => { }); it('renders correct button text when host not present', () => { - (useContext as jest.Mock).mockImplementation(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementation(() => ({ config: { host: '' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 482d68736af013..1d05128adc2e3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { snakeCase } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCard, EuiTextColor } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../../index'; - import { EuiButton } from '../../../shared/react_router_helpers'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; @@ -31,9 +30,7 @@ interface IProductCard { export const ProductCard: React.FC = ({ product, image }) => { const { http } = useValues(HttpLogic); - const { - config: { host }, - } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); const LAUNCH_BUTTON_TEXT = i18n.translate( 'xpack.enterpriseSearch.overview.productCard.launchButton', @@ -80,7 +77,7 @@ export const ProductCard: React.FC = ({ product, image }) => { }) } > - {host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT} + {config.host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT}
} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index 44efa57db897ff..f1f16d1a6f7a46 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/kea.mock'; -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; @@ -15,7 +16,7 @@ import { ProductCard } from '../product_card'; describe('ProductSelector', () => { it('renders the overview page and product cards with no host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); @@ -24,7 +25,7 @@ describe('ProductSelector', () => { describe('access checks when host is set', () => { beforeEach(() => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); }); it('does not render the App Search card if the user does not have access to AS', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 07b8d4b9926d7f..5c2d105e69c408 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -9,8 +9,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; - +import React from 'react'; +import { useValues } from 'kea'; import { EuiPage, EuiPageBody, @@ -24,10 +24,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { KibanaContext, IKibanaContext } from '../../../index'; - import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; - +import { KibanaLogic } from '../../../shared/kibana'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -45,12 +43,11 @@ interface IProductSelectorProps { export const ProductSelector: React.FC = ({ access }) => { const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - const { - config: { host }, - } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); - const shouldShowAppSearchCard = !host || hasAppSearchAccess; - const shouldShowWorkplaceSearchCard = !host || hasWorkplaceSearchAccess; + // If Enterprise Search hasn't been set up yet, show all products. Otherwise, only show products the user has access to + const shouldShowAppSearchCard = !config.host || hasAppSearchAccess; + const shouldShowWorkplaceSearchCard = !config.host || hasWorkplaceSearchAccess; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 2c0902163e3d6a..803d2c8462b1bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../__mocks__/shallow_usecontext.mock'; - -import React, { useContext } from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; @@ -19,12 +17,11 @@ import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; describe('EnterpriseSearch', () => { - beforeEach(() => { - (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); - }); - it('renders the Setup Guide and Product Selector', () => { + (useValues as jest.Mock).mockReturnValue({ + errorConnecting: false, + config: { host: 'localhost' }, + }); const wrapper = shallow(); expect(wrapper.find(SetupGuide)).toHaveLength(1); @@ -32,9 +29,10 @@ describe('EnterpriseSearch', () => { }); it('renders the error connecting prompt when host is not configured', () => { - (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); - + (useValues as jest.Mock).mockReturnValueOnce({ + errorConnecting: true, + config: { host: '' }, + }); const wrapper = shallow(); expect(wrapper.find(ErrorConnecting)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index e2c05434dd0bbc..7b97c6c9e58b6e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { KibanaContext, IKibanaContext } from '../index'; +import { KibanaLogic } from '../shared/kibana'; import { IInitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; @@ -23,7 +23,7 @@ import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { const { errorConnecting } = useValues(HttpLogic); - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); const showErrorConnecting = config.host && errorConnecting; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 2c6bc787923e36..63be9b684e56f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -7,28 +7,20 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; - import { Provider } from 'react-redux'; import { Store } from 'redux'; import { getContext, resetContext } from 'kea'; - import { I18nProvider } from '@kbn/i18n/react'; -import { AppMountParameters, CoreStart, ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; + +import { AppMountParameters, CoreStart } from 'src/core/public'; import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { IInitialAppData } from '../../common/types'; + +import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; import { mountHttpLogic } from './shared/http'; import { mountFlashMessagesLogic } from './shared/flash_messages'; import { externalUrl } from './shared/enterprise_search_url'; -import { IInitialAppData } from '../../common/types'; - -export interface IKibanaContext { - config: { host?: string }; - navigateToUrl: ApplicationStart['navigateToUrl']; - setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; - setDocTitle(title: string): void; -} - -export const KibanaContext = React.createContext({}); /** * This file serves as a reusable wrapper to share Kibana-level context and other helpers @@ -47,39 +39,37 @@ export const renderApp = ( resetContext({ createStore: true }); const store = getContext().store as Store; + const unmountKibanaLogic = mountKibanaLogic({ + config, + navigateToUrl: core.application.navigateToUrl, + setBreadcrumbs: core.chrome.setBreadcrumbs, + setDocTitle: core.chrome.docTitle.change, + }); const unmountLicensingLogic = mountLicensingLogic({ license$: plugins.licensing.license$, }); - const unmountHttpLogic = mountHttpLogic({ http: core.http, errorConnecting, readOnlyMode: initialData.readOnlyMode, }); - - const unmountFlashMessagesLogic = mountFlashMessagesLogic({ history: params.history }); + const unmountFlashMessagesLogic = mountFlashMessagesLogic({ + history: params.history, + }); ReactDOM.render( - - - - - - - + + + + + , params.element ); return () => { ReactDOM.unmountComponentAtNode(params.element); + unmountKibanaLogic(); unmountLicensingLogic(); unmountHttpLogic(); unmountFlashMessagesLogic(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index 29b773b80158af..25a02e847ccbd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index a2cb424dadee89..b92a5bbf1c64e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton } from '../react_router_helpers'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import './error_state_prompt.scss'; export const ErrorStatePrompt: React.FC = () => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return ( { + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + describe('mounts', () => { + it('sets values from props', () => { + mountKibanaLogic(mockKibanaValues); + + expect(KibanaLogic.values).toEqual(mockKibanaValues); + }); + + it('gracefully handles missing configs', () => { + mountKibanaLogic({ ...mockKibanaValues, config: undefined } as any); + + expect(KibanaLogic.values.config).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts new file mode 100644 index 00000000000000..a884acb02d10ae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.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 { kea, MakeLogicType } from 'kea'; + +import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; + +export interface IKibanaValues { + config: { host?: string }; + navigateToUrl: ApplicationStart['navigateToUrl']; + setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setDocTitle(title: string): void; +} + +export const KibanaLogic = kea>({ + path: ['enterprise_search', 'kibana_logic'], + reducers: ({ props }) => ({ + config: [props.config || {}, {}], + navigateToUrl: [props.navigateToUrl, {}], + setBreadcrumbs: [props.setBreadcrumbs, {}], + setDocTitle: [props.setDocTitle, {}], + }), +}); + +export const mountKibanaLogic = (props: IKibanaValues) => { + KibanaLogic(props); + const unmount = KibanaLogic.mount(); + return unmount; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 3c8b3a72188627..a2c0bcae6fc189 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import '../../__mocks__/react_router_history.mock'; -import { mockKibanaContext, mockHistory } from '../../__mocks__'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) })); import { letBrowserHandleEvent } from '../react_router_helpers'; @@ -53,7 +53,7 @@ describe('useBreadcrumbs', () => { const event = { preventDefault: jest.fn() }; breadcrumb.onClick(event); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test'); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test'); expect(mockHistory.createHref).toHaveBeenCalled(); expect(event.preventDefault).toHaveBeenCalled(); }); @@ -64,7 +64,7 @@ describe('useBreadcrumbs', () => { ])[0] as any; breadcrumb.onClick({ preventDefault: () => null }); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/test'); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test'); expect(mockHistory.createHref).not.toHaveBeenCalled(); }); @@ -74,7 +74,7 @@ describe('useBreadcrumbs', () => { (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); breadcrumb.onClick(); - expect(mockKibanaContext.navigateToUrl).not.toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); }); it('does not generate link behavior if path is excluded', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 19714608e73e9b..ff7f29e2e393c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext } from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import { ENTERPRISE_SEARCH_PLUGIN, @@ -34,7 +34,7 @@ export type TBreadcrumbs = IBreadcrumb[]; export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => { const history = useHistory(); - const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; + const { navigateToUrl } = useValues(KibanaLogic); return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { const breadcrumb = { text } as EuiBreadcrumb; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx index 61a066bb92216f..2aee224304f89b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/react_router_history.mock'; +import { mockKibanaValues } from '../../__mocks__'; import React from 'react'; - -import { mockKibanaContext, mountWithKibanaContext } from '../../__mocks__'; +import { shallow } from 'enzyme'; jest.mock('./generate_breadcrumbs', () => ({ useEnterpriseSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs), @@ -37,13 +38,13 @@ describe('Set Kibana Chrome helpers', () => { }); afterEach(() => { - expect(mockKibanaContext.setBreadcrumbs).toHaveBeenCalled(); - expect(mockKibanaContext.setDocTitle).toHaveBeenCalled(); + expect(mockKibanaValues.setBreadcrumbs).toHaveBeenCalled(); + expect(mockKibanaValues.setDocTitle).toHaveBeenCalled(); }); describe('SetEnterpriseSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(enterpriseSearchTitle).toHaveBeenCalledWith(['Hello World']); expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -55,7 +56,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(enterpriseSearchTitle).toHaveBeenCalledWith([]); expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([]); @@ -64,7 +65,7 @@ describe('Set Kibana Chrome helpers', () => { describe('SetAppSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(appSearchTitle).toHaveBeenCalledWith(['Engines']); expect(useAppSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -76,7 +77,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(appSearchTitle).toHaveBeenCalledWith([]); expect(useAppSearchBreadcrumbs).toHaveBeenCalledWith([]); @@ -85,7 +86,7 @@ describe('Set Kibana Chrome helpers', () => { describe('SetWorkplaceSearchChrome', () => { it('sets breadcrumbs and document title', () => { - mountWithKibanaContext(); + shallow(); expect(workplaceSearchTitle).toHaveBeenCalledWith(['Sources']); expect(useWorkplaceSearchBreadcrumbs).toHaveBeenCalledWith([ @@ -97,7 +98,7 @@ describe('Set Kibana Chrome helpers', () => { }); it('sets empty breadcrumbs and document title when isRoot is true', () => { - mountWithKibanaContext(); + shallow(); expect(workplaceSearchTitle).toHaveBeenCalledWith([]); expect(useWorkplaceSearchBreadcrumbs).toHaveBeenCalledWith([]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index 5e8d972e1a1355..2ae3ca0137d548 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../kibana'; + import { useEnterpriseSearchBreadcrumbs, useAppSearchBreadcrumbs, @@ -41,7 +43,7 @@ type TBreadcrumbsProps = IBreadcrumbsProps | IRootBreadcrumbsProps; export const SetEnterpriseSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = enterpriseSearchTitle(title as TTitle | []); @@ -59,7 +61,7 @@ export const SetEnterpriseSearchChrome: React.FC = ({ text, i export const SetAppSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = appSearchTitle(title as TTitle | []); @@ -77,7 +79,7 @@ export const SetAppSearchChrome: React.FC = ({ text, isRoot } export const SetWorkplaceSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); - const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); const title = isRoot ? [] : [text]; const docTitle = workplaceSearchTitle(title as TTitle | []); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 0c7bac99085dd5..eba632d86dc662 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; import '../../__mocks__/react_router_history.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; import { EuiLink, EuiButton } from '@elastic/eui'; -import { mockKibanaContext, mockHistory } from '../../__mocks__'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; @@ -69,7 +69,7 @@ describe('EUI & React Router Component Helpers', () => { wrapper.find(EuiLink).simulate('click', simulatedEvent); expect(simulatedEvent.preventDefault).toHaveBeenCalled(); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); }); it('does not prevent default browser behavior on new tab/window clicks', () => { @@ -81,7 +81,7 @@ describe('EUI & React Router Component Helpers', () => { }; wrapper.find(EuiLink).simulate('click', simulatedEvent); - expect(mockKibanaContext.navigateToUrl).not.toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); }); it('calls inherited onClick actions in addition to default navigation', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index e3b46632ddf9e6..99314515f2734f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { useHistory } from 'react-router-dom'; import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { KibanaLogic } from '../../shared/kibana'; import { letBrowserHandleEvent } from './link_events'; /** @@ -33,7 +34,7 @@ export const EuiReactRouterHelper: React.FC = ({ children, }) => { const history = useHistory(); - const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; + const { navigateToUrl } = useValues(KibanaLogic); // Generate the correct link href (with basename etc. accounted for) const href = shouldNotCreateHref ? to : history.createHref({ pathname: to }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx index 0423ae61779af1..802a10e3b3db76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiSteps, EuiIcon, EuiLink } from '@elastic/eui'; -import { mountWithContext } from '../../__mocks__'; +import { mountWithIntl } from '../../__mocks__'; import { SetupGuide } from './'; @@ -27,7 +27,7 @@ describe('SetupGuide', () => { }); it('renders with optional auth links', () => { - const wrapper = mountWithContext( + const wrapper = mountWithIntl( { it('renders WorkplaceSearchUnconfigured when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(WorkplaceSearchUnconfigured)).toHaveLength(1); }); it('renders WorkplaceSearchConfigured when config.host set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a68dfaf8ea4711..4769358a3eb306 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { IInitialAppData } from '../../../common/types'; -import { KibanaContext, IKibanaContext } from '../index'; +import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; @@ -24,7 +24,7 @@ import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; export const WorkplaceSearch: React.FC = (props) => { - const { config } = useContext(KibanaContext) as IKibanaContext; + const { config } = useValues(KibanaLogic); return !config.host ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx index ab5cd7f0de90fb..a757e187da0980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; - import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index d9b05c5da777df..d9d03245f6141d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 54066cee414d80..dc1c111dea0061 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -109,7 +109,7 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { complete: () => {}, }); }, - 250, + 350, [searchValue] ); diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts index 519e2861cdc1d4..bdc5714f7e2fed 100644 --- a/x-pack/plugins/ingest_manager/common/constants/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -13,3 +13,9 @@ export * from './epm'; export * from './output'; export * from './enrollment_api_key'; export * from './settings'; + +// TODO: This is the default `index.max_result_window` ES setting, which dictates +// the maximum amount of results allowed to be returned from a search. It's possible +// for the actual setting to differ from the default. Can we retrieve the real +// setting in the future? +export const SO_SEARCH_LIMIT = 10000; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 7ed2fed91aa93c..0709eddaa52ec9 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,7 +71,7 @@ export interface InstallPackageResponse { response: AssetReference[]; } -export interface IBulkInstallPackageError { +export interface IBulkInstallPackageHTTPError { name: string; statusCode: number; error: string | Error; @@ -86,7 +86,7 @@ export interface BulkInstallPackageInfo { } export interface BulkInstallPackagesResponse { - response: Array; + response: Array; } export interface BulkInstallPackagesRequest { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 185e1fa5eb0ce1..b97d39bac920b2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -7,6 +7,7 @@ export { PLUGIN_ID, EPM_API_ROUTES, AGENT_API_ROUTES, + SO_SEARCH_LIMIT, AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 1d80495d2b3471..b263f46b90a25c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -11,7 +11,7 @@ import { useCore } from './use_core'; const BASE_BREADCRUMB: ChromeBreadcrumb = { href: pagePathGetters.overview(), text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', { - defaultMessage: 'Ingest Manager', + defaultMessage: 'Fleet', }), }; @@ -155,21 +155,15 @@ const breadcrumbGetters: { fleet: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { - defaultMessage: 'Fleet', + text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + defaultMessage: 'Agents', }), }, ], fleet_agent_list: () => [ BASE_BREADCRUMB, { - href: pagePathGetters.fleet(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { - defaultMessage: 'Fleet', - }), - }, - { - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, @@ -178,12 +172,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.fleet(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { - defaultMessage: 'Fleet', - }), - }, - { - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, @@ -193,12 +182,12 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.fleet(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { - defaultMessage: 'Fleet', + text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + defaultMessage: 'Agents', }), }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', { + text: i18n.translate('xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle', { defaultMessage: 'Enrollment tokens', }), }, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 7da83307405322..5de47ee4f410ba 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -83,8 +83,8 @@ export const DefaultLayout: React.FunctionComponent = ({ disabled={!fleet?.enabled} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx index 25684c9faf5946..ee453b9e786f11 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx @@ -15,7 +15,8 @@ import { EuiIcon, EuiPortal, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n/react'; +import { SO_SEARCH_LIMIT } from '../../../../constants'; import { Agent } from '../../../../types'; import { AgentReassignAgentPolicyFlyout, AgentUnenrollAgentModal } from '../../components'; @@ -153,11 +154,22 @@ export const AgentBulkActions: React.FunctionComponent<{ - + {totalAgents > SO_SEARCH_LIMIT ? ( + , + total: , + }} + /> + ) : ( + + )} {(selectionMode === 'manual' && selectedAgents.length) || @@ -184,7 +196,7 @@ export const AgentBulkActions: React.FunctionComponent<{ count: selectionMode === 'manual' ? selectedAgents.length - : totalAgents - totalInactiveAgents, + : Math.min(totalAgents - totalInactiveAgents, SO_SEARCH_LIMIT), }} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx index 7f23c645f9a2ea..874d42a8db0954 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { SO_SEARCH_LIMIT } from '../../../../constants'; import { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../../../types'; import { sendGetEnrollmentAPIKeys, useCore } from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; @@ -98,7 +99,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { try { const res = await sendGetEnrollmentAPIKeys({ page: 1, - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); if (res.error) { throw res.error; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx index 04fef7f4b3f21d..c840b487a39704 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx @@ -74,14 +74,14 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { ) : ( <> ), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index b01dbbd57c16fd..278beb5dfe35f1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -126,7 +126,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {

- +

@@ -134,7 +134,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx index 617be92b3b1fe3..e54eff1cbd4a54 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; +import { SO_SEARCH_LIMIT } from '../../../constants'; import { useLink, useGetPackagePolicies } from '../../../hooks'; import { AgentPolicy } from '../../../types'; import { Loading } from '../../fleet/components'; @@ -25,7 +26,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = const { getHref } = useLink(); const packagePoliciesRequest = useGetPackagePolicies({ page: 1, - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx index d7b08bf5ffa3a3..482105cdea3006 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -25,8 +25,8 @@ export const OverviewAgentSection = () => { return ( {

diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 5f7bfe865e8921..59741ce79dd8bd 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -78,7 +78,7 @@ export class IngestManagerPlugin core.application.register({ id: PLUGIN_ID, category: DEFAULT_APP_CATEGORIES.management, - title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Ingest Manager' }), + title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Fleet' }), order: 9020, euiIconType: 'logoElastic', async mount(params: AppMountParameters) { diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index d677b79bb46f8d..3965e27da05427 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -31,6 +31,7 @@ export { SETTINGS_API_ROUTES, APP_API_ROUTES, // Saved object types + SO_SEARCH_LIMIT, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 7ae896c1f30a6a..c55979d187f9df 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -13,7 +13,9 @@ import { GetCategoriesResponse, GetPackagesResponse, GetLimitedPackagesResponse, + BulkInstallPackageInfo, BulkInstallPackagesResponse, + IBulkInstallPackageHTTPError, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -26,21 +28,21 @@ import { BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; import { + BulkInstallResponse, + bulkInstallPackages, getCategories, getPackages, getFile, getPackageInfo, + handleInstallPackageFailure, installPackage, + isBulkInstallError, removeInstallation, getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; -import { - handleInstallPackageFailure, - bulkInstallPackages, -} from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -171,6 +173,21 @@ export const installPackageFromRegistryHandler: RequestHandler< } }; +const bulkInstallServiceResponseToHttpEntry = ( + result: BulkInstallResponse +): BulkInstallPackageInfo | IBulkInstallPackageHTTPError => { + if (isBulkInstallError(result)) { + const { statusCode, body } = ingestErrorToResponseOptions(result.error); + return { + name: result.name, + statusCode, + error: body.message, + }; + } else { + return result; + } +}; + export const bulkInstallPackagesFromRegistryHandler: RequestHandler< undefined, undefined, @@ -178,13 +195,14 @@ export const bulkInstallPackagesFromRegistryHandler: RequestHandler< > = async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const res = await bulkInstallPackages({ + const bulkInstalledResponses = await bulkInstallPackages({ savedObjectsClient, callCluster, packagesToUpgrade: request.body.packages, }); + const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { - response: res, + response: payload, }; return response.ok({ body }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.ts new file mode 100644 index 00000000000000..af937c55930825 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/bulk_install_packages.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 { SavedObjectsClientContract } from 'src/core/server'; +import { CallESAsCurrentUser } from '../../../types'; +import * as Registry from '../registry'; +import { getInstallationObject } from './index'; +import { BulkInstallResponse, IBulkInstallPackageError, upgradePackage } from './install'; + +interface BulkInstallPackagesParams { + savedObjectsClient: SavedObjectsClientContract; + packagesToUpgrade: string[]; + callCluster: CallESAsCurrentUser; +} + +export async function bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade, + callCluster, +}: BulkInstallPackagesParams): Promise { + const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => + Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), + Registry.fetchFindLatestPackage(pkgToUpgrade), + ]) + ); + const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); + const installResponsePromises = installedAndLatestResults.map(async (result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + const [installedPkg, latestPkg] = result.value; + return upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, + }); + } else { + return { name: pkgToUpgrade, error: result.reason }; + } + }); + const installResults = await Promise.allSettled(installResponsePromises); + const installResponses = installResults.map((result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + return result.value; + } else { + return { name: pkgToUpgrade, error: result.reason }; + } + }); + + return installResponses; +} + +export function isBulkInstallError(test: any): test is IBulkInstallPackageError { + return 'error' in test && test.error instanceof Error; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts new file mode 100644 index 00000000000000..f0b487ad597745 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; + +jest.mock('./install'); +jest.mock('./bulk_install_packages'); +jest.mock('./get'); + +import { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; +const { ensureInstalledDefaultPackages } = jest.requireActual('./install'); +const { isBulkInstallError: actualIsBulkInstallError } = jest.requireActual( + './bulk_install_packages' +); +import { getInstallation } from './get'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { appContextService } from '../../app_context'; +import { createAppContextStartContractMock } from '../../../mocks'; + +// if we add this assertion, TS will type check the return value +// and the editor will also know about .mockImplementation, .mock.calls, etc +const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction< + typeof bulkInstallPackages +>; +const mockedIsBulkInstallError = isBulkInstallError as jest.MockedFunction< + typeof isBulkInstallError +>; +const mockedGetInstallation = getInstallation as jest.MockedFunction; + +// I was unable to get the actual implementation set in the `jest.mock()` call at the top to work +// so this will set the `isBulkInstallError` function back to the actual implementation +mockedIsBulkInstallError.mockImplementation(actualIsBulkInstallError); + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test package', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; + +describe('ensureInstalledDefaultPackages', () => { + let soClient: jest.Mocked; + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(async () => { + appContextService.stop(); + }); + it('should return an array of Installation objects when successful', async () => { + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: mockInstallation.attributes.name, + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const resp = await ensureInstalledDefaultPackages(soClient, jest.fn()); + expect(resp).toEqual([mockInstallation.attributes]); + }); + it('should throw the first Error it finds', async () => { + class SomeCustomError extends Error {} + mockedGetInstallation.mockImplementation(async () => { + return mockInstallation.attributes; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'success one', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'success two', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure one', + error: new SomeCustomError('abc 123'), + }, + { + name: 'success three', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + { + name: 'failure two', + error: new Error('zzz'), + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(2); + expect(installPromise).rejects.toThrow(SomeCustomError); + expect(installPromise).rejects.toThrow('abc 123'); + }); + it('should throw an error when get installation returns undefined', async () => { + mockedGetInstallation.mockImplementation(async () => { + return undefined; + }); + mockedBulkInstallPackages.mockImplementationOnce(async function () { + return [ + { + name: 'undefined package', + assets: [], + newVersion: '', + oldVersion: '', + statusCode: 200, + }, + ]; + }); + const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); + expect.assertions(1); + expect(installPromise).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.ts new file mode 100644 index 00000000000000..cce4b7fee8fd7c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_install_type.test.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 { SavedObject } from 'src/core/server'; +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { getInstallType } from './install'; + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; +const mockInstallationUpdateFail: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installing', + install_version: '1.0.1', + install_started_at: new Date().toISOString(), + }, +}; + +describe('getInstallType', () => { + it('should return correct type when installing and no other version is currently installed', () => { + const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); + expect(installTypeInstall).toBe('install'); + + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'update').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reinstall').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reupdate').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'rollback').toBe(false); + }); + + it('should return correct type when installing the same version', () => { + const installTypeReinstall = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallation, + }); + expect(installTypeReinstall).toBe('reinstall'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReinstall === 'install').toBe(false); + }); + + it('should return correct type when moving from one version to another', () => { + const installTypeUpdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallation, + }); + expect(installTypeUpdate).toBe('update'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeUpdate === 'install').toBe(false); + }); + + it('should return correct type when update fails and trys again', () => { + const installTypeReupdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeReupdate).toBe('reupdate'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReupdate === 'install').toBe(false); + }); + + it('should return correct type when attempting to rollback from a failed update', () => { + const installTypeRollback = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeRollback).toBe('rollback'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeRollback === 'install').toBe(false); + }); +}); 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 57c4f77432455d..94aa969c2d2b88 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 @@ -12,6 +12,8 @@ import { InstallationStatus, KibanaAssetType, } from '../../../types'; + +export { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; export { getCategories, getFile, @@ -23,7 +25,13 @@ export { SearchParams, } from './get'; -export { installPackage, ensureInstalledPackage } from './install'; +export { + BulkInstallResponse, + handleInstallPackageFailure, + installPackage, + IBulkInstallPackageError, + ensureInstalledPackage, +} from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts deleted file mode 100644 index 2f60c74d3514f9..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; -import { SavedObject } from 'src/core/server'; -import { getInstallType } from './install'; - -const mockInstallation: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installed', - install_version: '1.0.0', - install_started_at: new Date().toISOString(), - }, -}; -const mockInstallationUpdateFail: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test packagek', - version: '1.0.0', - install_status: 'installing', - install_version: '1.0.1', - install_started_at: new Date().toISOString(), - }, -}; -describe('install', () => { - describe('getInstallType', () => { - it('should return correct type when installing and no other version is currently installed', () => { - const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); - expect(installTypeInstall).toBe('install'); - - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'update').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reinstall').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'reupdate').toBe(false); - // @ts-expect-error can only be 'install' if no installedPkg given - expect(installTypeInstall === 'rollback').toBe(false); - }); - - it('should return correct type when installing the same version', () => { - const installTypeReinstall = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallation, - }); - expect(installTypeReinstall).toBe('reinstall'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReinstall === 'install').toBe(false); - }); - - it('should return correct type when moving from one version to another', () => { - const installTypeUpdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallation, - }); - expect(installTypeUpdate).toBe('update'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeUpdate === 'install').toBe(false); - }); - - it('should return correct type when update fails and trys again', () => { - const installTypeReupdate = getInstallType({ - pkgVersion: '1.0.1', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeReupdate).toBe('reupdate'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeReupdate === 'install').toBe(false); - }); - - it('should return correct type when attempting to rollback from a failed update', () => { - const installTypeRollback = getInstallType({ - pkgVersion: '1.0.0', - installedPkg: mockInstallationUpdateFail, - }); - expect(installTypeRollback).toBe('rollback'); - - // @ts-expect-error cannot be 'install' if given installedPkg - expect(installTypeRollback === 'install').toBe(false); - }); - }); -}); 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 800151a41a4299..6187f683a3a80d 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 @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; import Boom from 'boom'; import { UnwrapPromise } from '@kbn/utility-types'; -import { BulkInstallPackageInfo, IBulkInstallPackageError } from '../../../../common'; +import { BulkInstallPackageInfo } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -23,7 +23,13 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; +import { + getInstallation, + getInstallationObject, + isRequiredPackage, + bulkInstallPackages, + isBulkInstallError, +} from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; @@ -36,11 +42,7 @@ import { } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; -import { - IngestManagerError, - PackageOutdatedError, - ingestErrorToResponseOptions, -} from '../../../errors'; +import { IngestManagerError, PackageOutdatedError } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; import { appContextService } from '../../app_context'; @@ -68,17 +70,27 @@ export async function ensureInstalledDefaultPackages( callCluster: CallESAsCurrentUser ): Promise { const installations = []; - for (const pkgName in DefaultPackages) { - if (!DefaultPackages.hasOwnProperty(pkgName)) continue; - const installation = ensureInstalledPackage({ - savedObjectsClient, - pkgName, - callCluster, - }); - installations.push(installation); + const bulkResponse = await bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade: Object.values(DefaultPackages), + callCluster, + }); + + for (const resp of bulkResponse) { + if (isBulkInstallError(resp)) { + throw resp.error; + } else { + installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name })); + } } - return Promise.all(installations); + const retrievedInstallations = await Promise.all(installations); + return retrievedInstallations.map((installation, index) => { + if (!installation) { + throw new Error(`could not get installation ${bulkResponse[index].name}`); + } + return installation; + }); } export async function ensureInstalledPackage(options: { @@ -154,21 +166,11 @@ export async function handleInstallPackageFailure({ } } -type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; -function bulkInstallErrorToOptions({ - pkgToUpgrade, - error, -}: { - pkgToUpgrade: string; +export interface IBulkInstallPackageError { + name: string; error: Error; -}): IBulkInstallPackageError { - const { statusCode, body } = ingestErrorToResponseOptions(error); - return { - name: pkgToUpgrade, - statusCode, - error: body.message, - }; } +export type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; interface UpgradePackageParams { savedObjectsClient: SavedObjectsClientContract; @@ -177,7 +179,7 @@ interface UpgradePackageParams { latestPkg: UnwrapPromise>; pkgToUpgrade: string; } -async function upgradePackage({ +export async function upgradePackage({ savedObjectsClient, callCluster, installedPkg, @@ -207,7 +209,7 @@ async function upgradePackage({ installedPkg, callCluster, }); - return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + return { name: pkgToUpgrade, error: installFailed }; } } else { // package was already at the latest version @@ -223,51 +225,6 @@ async function upgradePackage({ } } -interface BulkInstallPackagesParams { - savedObjectsClient: SavedObjectsClientContract; - packagesToUpgrade: string[]; - callCluster: CallESAsCurrentUser; -} -export async function bulkInstallPackages({ - savedObjectsClient, - packagesToUpgrade, - callCluster, -}: BulkInstallPackagesParams): Promise { - const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => - Promise.all([ - getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), - Registry.fetchFindLatestPackage(pkgToUpgrade), - ]) - ); - const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); - const installResponsePromises = installedAndLatestResults.map(async (result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - const [installedPkg, latestPkg] = result.value; - return upgradePackage({ - savedObjectsClient, - callCluster, - installedPkg, - latestPkg, - pkgToUpgrade, - }); - } else { - return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); - } - }); - const installResults = await Promise.allSettled(installResponsePromises); - const installResponses = installResults.map((result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - return result.value; - } else { - return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); - } - }); - - return installResponses; -} - interface InstallPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.ts index 06772206d51980..77c0e446d5c238 100644 --- a/x-pack/plugins/ingest_manager/server/services/saved_object.ts +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; +import { SO_SEARCH_LIMIT } from '../constants'; import { ListWithKuery } from '../types'; /** @@ -40,19 +41,13 @@ export const findAllSOs = async ( const { type, sortField, sortOrder, kuery } = options; let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; - // TODO: This is the default `index.max_result_window` ES setting, which dictates - // the maximum amount of results allowed to be returned from a search. It's possible - // for the actual setting to differ from the default. Can we retrieve the real - // setting in the future? - const searchLimit = 10000; - const query = { type, sortField, sortOrder, filter: kuery, page: 1, - perPage: searchLimit, + perPage: SO_SEARCH_LIMIT, }; const { saved_objects: initialSOs, total } = await soClient.find(query); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index f02057bae15981..c7ecf843d6a51e 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -22,6 +22,7 @@ import { Output, DEFAULT_AGENT_POLICIES_PACKAGES, } from '../../common'; +import { SO_SEARCH_LIMIT } from '../constants'; import { getPackageInfo } from './epm/packages'; import { packagePolicyService } from './package_policy'; import { generateEnrollmentAPIKey } from './api_keys'; @@ -159,7 +160,7 @@ export async function setupFleet( }); const { items: agentPolicies } = await agentPolicyService.list(soClient, { - perPage: 10000, + perPage: SO_SEARCH_LIMIT, }); await Promise.all( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx index 215ef63d9782ef..7f1b203869c70d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx @@ -179,6 +179,41 @@ const createActions = (testBed: TestBed) => { }); }, + clickDocumentsDropdown() { + act(() => { + find('documentsDropdown.documentsButton').simulate('click'); + }); + component.update(); + }, + + clickEditDocumentsButton() { + act(() => { + find('editDocumentsButton').simulate('click'); + }); + component.update(); + }, + + clickClearAllButton() { + act(() => { + find('clearAllDocumentsButton').simulate('click'); + }); + component.update(); + }, + + async clickConfirmResetButton() { + const modal = document.body.querySelector( + '[data-test-subj="resetDocumentsConfirmationModal"]' + ); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + + await act(async () => { + confirmButton!.click(); + }); + component.update(); + }, + async clickProcessor(processorSelector: string) { await act(async () => { find(`${processorSelector}.manageItemButton`).simulate('click'); @@ -230,6 +265,7 @@ type TestSubject = | 'addDocumentsButton' | 'testPipelineFlyout' | 'documentsDropdown' + | 'documentsDropdown.documentsButton' | 'outputTab' | 'documentsEditor' | 'runPipelineButton' @@ -248,6 +284,8 @@ type TestSubject = | 'configurationTab' | 'outputTab' | 'processorOutputTabContent' + | 'editDocumentsButton' + | 'clearAllDocumentsButton' | 'addDocumentsAccordion' | 'addDocumentButton' | 'addDocumentError' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx index 47f05602799e4e..69a1b7c2c126d5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx @@ -22,6 +22,27 @@ describe('Test pipeline', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); + // This is a hack + // We need to provide the processor id in the mocked output; + // this is generated dynamically + // As a workaround, the value is added as a data attribute in the UI + // and we retrieve it to generate the mocked output. + const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => { + const { find } = testBed; + + const docs = output.docs.map((doc) => { + const results = doc.processor_results.map((result, index) => { + const tag = find(`processors>${index}`).props()['data-processor-id']; + return { + ...result, + tag, + }; + }); + return { processor_results: results }; + }); + return { docs }; + }; + beforeAll(() => { jest.useFakeTimers(); }); @@ -236,30 +257,77 @@ describe('Test pipeline', () => { expect(find('addDocumentError').text()).toContain(error.message); }); }); - }); - describe('Processors', () => { - // This is a hack - // We need to provide the processor id in the mocked output; - // this is generated dynamically and not something we can stub. - // As a workaround, the value is added as a data attribute in the UI - // and we retrieve it to generate the mocked output. - const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => { - const { find } = testBed; + describe('Documents dropdown', () => { + beforeEach(async () => { + const { actions } = testBed; - const docs = output.docs.map((doc) => { - const results = doc.processor_results.map((result, index) => { - const tag = find(`processors>${index}`).props()['data-processor-id']; - return { - ...result, - tag, - }; - }); - return { processor_results: results }; + httpRequestsMockHelpers.setSimulatePipelineResponse( + addProcessorTagtoMockOutput(SIMULATE_RESPONSE) + ); + + // Open flyout + actions.clickAddDocumentsButton(); + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + // Close flyout + actions.closeTestPipelineFlyout(); }); - return { docs }; - }; + it('should open flyout to edit documents', () => { + const { exists, actions } = testBed; + + // Dropdown should be visible + expect(exists('documentsDropdown')).toBe(true); + + // Open dropdown and edit documents + actions.clickDocumentsDropdown(); + actions.clickEditDocumentsButton(); + + // Flyout should be visible with "Documents" tab enabled + expect(exists('testPipelineFlyout')).toBe(true); + expect(exists('documentsTabContent')).toBe(true); + }); + + it('should clear all documents and stop pipeline simulation', async () => { + const { exists, actions, find } = testBed; + + // Dropdown should be visible and processor status should equal "success" + expect(exists('documentsDropdown')).toBe(true); + const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props()[ + 'aria-label' + ]; + expect(initialProcessorStatusLabel).toEqual('Success'); + + // Open flyout and click clear all button + actions.clickDocumentsDropdown(); + actions.clickEditDocumentsButton(); + actions.clickClearAllButton(); + + // Verify modal + const modal = document.body.querySelector( + '[data-test-subj="resetDocumentsConfirmationModal"]' + ); + + expect(modal).not.toBe(null); + expect(modal!.textContent).toContain('Clear documents'); + + // Confirm reset and close modal + await actions.clickConfirmResetButton(); + + // Verify documents and processors were reset + expect(exists('documentsDropdown')).toBe(false); + expect(exists('addDocumentsButton')).toBe(true); + const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props()[ + 'aria-label' + ]; + expect(resetProcessorStatusIconLabel).toEqual('Not run'); + }); + }); + }); + + describe('Processors', () => { it('should show "inactive" processor status by default', async () => { const { find } = testBed; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx index d9feaaffa5aece..3df73b54b8ccec 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx @@ -102,7 +102,7 @@ export const EditProcessorForm: FunctionComponent = ({ handleSubmit, resetProcessors, }) => { - const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); + const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext(); const { testOutputPerProcessor, config: { selectedDocumentIndex, documents }, @@ -117,7 +117,7 @@ export const EditProcessorForm: FunctionComponent = ({ testOutputPerProcessor[selectedDocumentIndex][processor.id]; const updateSelectedDocument = (index: number) => { - setCurrentTestPipelineData({ + testPipelineDataDispatch({ type: 'updateActiveDocument', payload: { config: { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx index 26492454cbcf5d..1a8f1258404e76 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; -import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; +import { TestPipelineFlyoutTab } from './test_pipeline_tabs'; const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx index 269a697a33c17f..9c8d6c8bdb289b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx @@ -8,18 +8,15 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiButton, EuiPopover, + EuiPopoverFooter, EuiButtonEmpty, EuiPopoverTitle, EuiSelectable, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, } from '@elastic/eui'; import { Document } from '../../../types'; -import { TestPipelineFlyoutTab } from '../test_pipeline_flyout_tabs'; +import { TestPipelineFlyoutTab } from '../test_pipeline_tabs'; import './documents_dropdown.scss'; @@ -31,9 +28,9 @@ const i18nTexts = { } ), addDocumentsButtonLabel: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.buttonLabel', + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.editDocumentsButtonLabel', { - defaultMessage: 'Add documents', + defaultMessage: 'Edit documents', } ), popoverTitle: i18n.translate( @@ -88,8 +85,10 @@ export const DocumentsDropdown: FunctionComponent = ({ > ({ key: index.toString(), + 'data-test-subj': 'documentListItem', checked: selectedDocumentIndex === index ? 'on' : undefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel', { defaultMessage: 'Document {documentNumber}', @@ -107,32 +106,27 @@ export const DocumentsDropdown: FunctionComponent = ({ setShowPopover(false); }} > - {(list, search) => ( -
+ {(list) => ( + <> {i18nTexts.popoverTitle} {list} -
+ )}
- - - - - { - openFlyout('documents'); - setShowPopover(false); - }} - data-test-subj="addDocumentsButton" - > - {i18nTexts.addDocumentsButtonLabel} - - - - - + + { + openFlyout('documents'); + setShowPopover(false); + }} + data-test-subj="editDocumentsButton" + > + {i18nTexts.addDocumentsButtonLabel} + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx index 90180422295904..d2fc9c51be6994 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiButton } from '@elastic/eui'; -import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; +import { TestPipelineFlyoutTab } from './test_pipeline_tabs'; const i18nTexts = { buttonLabel: i18n.translate( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx index cec02db26729d9..83a9303859d2a5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context'; import { DocumentsDropdown } from './documents_dropdown'; -import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; +import { TestPipelineFlyoutTab } from './test_pipeline_tabs'; import { AddDocumentsButton } from './add_documents_button'; import { TestOutputButton } from './test_output_button'; import { TestPipelineFlyout } from './test_pipeline_flyout.container'; @@ -24,7 +24,7 @@ const i18nTexts = { }; export const TestPipelineActions: FunctionComponent = () => { - const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); + const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext(); const { state: { processors }, @@ -39,7 +39,7 @@ export const TestPipelineActions: FunctionComponent = () => { const [activeFlyoutTab, setActiveFlyoutTab] = useState('documents'); const updateSelectedDocument = (index: number) => { - setCurrentTestPipelineData({ + testPipelineDataDispatch({ type: 'updateActiveDocument', payload: { config: { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx index 23dda55db41f81..e7ccb9d17f2b15 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx @@ -15,8 +15,7 @@ import { Document } from '../../types'; import { useIsMounted } from '../../use_is_mounted'; import { TestPipelineFlyout as ViewComponent } from './test_pipeline_flyout'; -import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; -import { documentsSchema } from './test_pipeline_flyout_tabs/documents_schema'; +import { TestPipelineFlyoutTab } from './test_pipeline_tabs'; export interface Props { activeTab: TestPipelineFlyoutTab; @@ -39,7 +38,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ const { testPipelineData, - setCurrentTestPipelineData, + testPipelineDataDispatch, updateTestOutputPerProcessor, } = useTestPipelineContext(); @@ -48,7 +47,6 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ } = testPipelineData; const { form } = useForm({ - schema: documentsSchema, defaultValue: { documents: cachedDocuments || '', }, @@ -88,7 +86,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ // reset the per-processor output // this is needed in the scenario where the pipeline has already executed, // but you modified the sample documents and there was an error on re-execution - setCurrentTestPipelineData({ + testPipelineDataDispatch({ type: 'updateOutputPerProcessor', payload: { isExecutingPipeline: false, @@ -99,7 +97,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ return { isSuccessful: false }; } - setCurrentTestPipelineData({ + testPipelineDataDispatch({ type: 'updateConfig', payload: { config: { @@ -133,7 +131,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ processors, services.api, services.notifications.toasts, - setCurrentTestPipelineData, + testPipelineDataDispatch, updateTestOutputPerProcessor, ] ); @@ -157,6 +155,12 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ } }; + const resetTestOutput = () => { + testPipelineDataDispatch({ + type: 'reset', + }); + }; + useEffect(() => { if (cachedDocuments && activeTab === 'output') { handleTestPipeline({ documents: cachedDocuments, verbose: cachedVerbose }, true); @@ -169,6 +173,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ return ( void; handleTestPipeline: ( @@ -31,11 +30,14 @@ export interface Props { cachedVerbose?: boolean; cachedDocuments?: Document[]; testOutput?: any; - form: FormHook; + form: FormHook<{ + documents: string | Document[]; + }>; validateAndTestPipeline: () => Promise; selectedTab: TestPipelineFlyoutTab; setSelectedTab: (selectedTa: TestPipelineFlyoutTab) => void; testingError: any; + resetTestOutput: () => void; } export interface TestPipelineConfig { @@ -45,6 +47,7 @@ export interface TestPipelineConfig { export const TestPipelineFlyout: React.FunctionComponent = ({ handleTestPipeline, + resetTestOutput, isRunningTest, cachedVerbose, cachedDocuments, @@ -75,6 +78,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ form={form} validateAndTestPipeline={validateAndTestPipeline} isRunningTest={isRunningTest} + resetTestOutput={resetTestOutput} /> ); } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/documents_schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/documents_schema.tsx deleted file mode 100644 index d0e0596375cb22..00000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/documents_schema.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 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: ( - - {JSON.stringify([ - { - _index: 'index', - _id: 'id', - _source: { - foo: 'bar', - }, - }, - ])} - - ), - }} - /> - ), - 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) => { - const parsedJSON = JSON.parse(value); - - if (!parsedJSON.length) { - return { - message: i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', - { - defaultMessage: 'At least one document is required.', - } - ), - }; - } - }, - }, - { - validator: ({ value }: ValidationFuncArg) => { - const parsedJSON = JSON.parse(value); - - const isMissingSourceField = parsedJSON.find((document: { _source?: object }) => { - if (!document._source) { - return true; - } - - return false; - }); - - if (isMissingSourceField) { - return { - message: i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.sourceFieldRequiredError', - { - defaultMessage: 'Documents require a _source field.', - } - ), - }; - } - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx deleted file mode 100644 index 6fd340054d2a4d..00000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or 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, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { EuiSpacer, EuiText, EuiButton, EuiLink } from '@elastic/eui'; - -import { - getUseField, - Field, - JsonEditorField, - useKibana, - useFormData, - FormHook, - Form, -} from '../../../../../../shared_imports'; - -import { AddDocumentsAccordion } from './add_documents_accordion'; - -const UseField = getUseField({ component: Field }); - -interface Props { - validateAndTestPipeline: () => Promise; - isRunningTest: boolean; - form: FormHook; -} - -export const DocumentsTab: FunctionComponent = ({ - validateAndTestPipeline, - isRunningTest, - form, -}) => { - const { services } = useKibana(); - - const [, formatData] = useFormData({ form }); - - const onAddDocumentHandler = useCallback( - (document) => { - const { documents: existingDocuments = [] } = formatData(); - - form.reset({ defaultValue: { documents: [...existingDocuments, document] } }); - }, - [form, formatData] - ); - - return ( -
-
- -

- - {i18n.translate( - 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', - { - defaultMessage: 'Learn more.', - } - )} - - ), - }} - /> -

-
- - - - - - - - {/* Documents editor */} - - - - - - {isRunningTest ? ( - - ) : ( - - )} - -
-
- ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_document_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx similarity index 97% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_document_form.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx index 340cf1af92300f..7bb860facfeb29 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_document_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_document_form.tsx @@ -25,9 +25,9 @@ import { TextField, fieldValidators, FieldConfig, -} from '../../../../../../shared_imports'; -import { useIsMounted } from '../../../use_is_mounted'; -import { Document } from '../../../types'; +} from '../../../../../../../shared_imports'; +import { useIsMounted } from '../../../../use_is_mounted'; +import { Document } from '../../../../types'; const UseField = getUseField({ component: Field }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/add_documents_accordion.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/add_documents_accordion.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/add_documents_accordion.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx similarity index 88% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/add_documents_accordion.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx index 88ced6e9e94dd0..c425a41fe6ee95 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/add_documents_accordion.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/add_documents_accordion.tsx @@ -11,8 +11,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; import { UrlGeneratorsDefinition } from 'src/plugins/share/public'; -import { useKibana } from '../../../../../../../shared_imports'; -import { useIsMounted } from '../../../../use_is_mounted'; +import { useKibana } from '../../../../../../../../shared_imports'; +import { useIsMounted } from '../../../../../use_is_mounted'; import { AddDocumentForm } from '../add_document_form'; import './add_documents_accordion.scss'; @@ -26,6 +26,12 @@ const i18nTexts = { defaultMessage: 'Add documents from index', } ), + addDocumentsDescription: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText', + { + defaultMessage: 'Provide the index name and document ID of the indexed document to test.', + } + ), }; interface Props { @@ -79,10 +85,7 @@ export const AddDocumentsAccordion: FunctionComponent = ({ onAddDocuments

- + {i18nTexts.addDocumentsDescription} {discoverLink && ( <> {' '} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/add_documents_accordion/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/add_documents_accordion/index.ts diff --git a/x-pack/plugins/ml/public/application/overview/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts similarity index 83% rename from x-pack/plugins/ml/public/application/overview/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts index 7d99bb10940154..1c3b6df577f4dd 100644 --- a/x-pack/plugins/ml/public/application/overview/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { OverviewPage } from './overview_page'; +export { DocumentsTab } from './tab_documents'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx new file mode 100644 index 00000000000000..d3f969c5ebb232 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx @@ -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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +interface Props { + confirmResetTestOutput: () => void; + closeModal: () => void; +} + +const i18nTexts = { + modalTitle: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.title', + { + defaultMessage: 'Clear documents', + } + ), + modalDescription: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.description', + { + defaultMessage: 'This will stop pipeline simulation.', + } + ), + cancelButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + resetButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.resetButtonLabel', + { + defaultMessage: 'Clear documents', + } + ), +}; + +export const ResetDocumentsModal: FunctionComponent = ({ + confirmResetTestOutput, + closeModal, +}) => { + return ( + + +

{i18nTexts.modalDescription}

+ + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss new file mode 100644 index 00000000000000..c07f58d280e2a2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.scss @@ -0,0 +1,11 @@ +.documentsTab { + &__documentField { + position: relative; + + &__button { + position: absolute; + right: 0; + top: 0; + } + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx new file mode 100644 index 00000000000000..ae784472ebbd9f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or 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, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiText, EuiButton, EuiLink, EuiCode, EuiButtonEmpty } from '@elastic/eui'; + +import { parseJson, stringifyJson } from '../../../../../../lib'; +import { + getUseField, + Field, + JsonEditorField, + useKibana, + FieldConfig, + fieldValidators, + ValidationFuncArg, + FormHook, + Form, + useFormData, +} from '../../../../../../../shared_imports'; +import { Document } from '../../../../types'; +import { AddDocumentsAccordion } from './add_documents_accordion'; +import { ResetDocumentsModal } from './reset_documents_modal'; + +import './tab_documents.scss'; + +const UseField = getUseField({ component: Field }); + +const { emptyField, isJsonField } = fieldValidators; + +interface Props { + validateAndTestPipeline: () => Promise; + resetTestOutput: () => void; + isRunningTest: boolean; + form: FormHook<{ + documents: string | Document[]; + }>; +} + +const i18nTexts = { + learnMoreLink: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', + { + defaultMessage: 'Learn more.', + } + ), + documentsEditorAriaLabel: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel', + { + defaultMessage: 'Documents JSON editor', + } + ), + documentsEditorClearAllButton: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldClearAllButtonLabel', + { + defaultMessage: 'Clear all', + } + ), + runButton: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel', + { + defaultMessage: 'Run the pipeline', + } + ), + runningButton: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel', + { + defaultMessage: 'Running', + } + ), +}; + +const documentFieldConfig: FieldConfig = { + label: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', + { + defaultMessage: 'Documents', + } + ), + helpText: ( + + {JSON.stringify([ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + ])} + + ), + }} + /> + ), + 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) => { + const parsedJSON = JSON.parse(value); + + if (!parsedJSON.length) { + return { + message: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', + { + defaultMessage: 'At least one document is required.', + } + ), + }; + } + }, + }, + ], +}; + +export const DocumentsTab: FunctionComponent = ({ + validateAndTestPipeline, + isRunningTest, + form, + resetTestOutput, +}) => { + const { services } = useKibana(); + + const [, formatData] = useFormData({ form }); + + const onAddDocumentHandler = useCallback( + (document) => { + const { documents: existingDocuments = [] } = formatData(); + + form.reset({ defaultValue: { documents: [...existingDocuments, document] } }); + }, + [form, formatData] + ); + + const [showResetModal, setShowResetModal] = useState(false); + + return ( +
+
+ +

+ + {i18nTexts.learnMoreLink} + + ), + }} + /> +

+
+ + + + + + + + {/* Documents editor */} + + {(field) => ( +
+ setShowResetModal(true)} + data-test-subj="clearAllDocumentsButton" + className="documentsTab__documentField__button" + > + {i18nTexts.documentsEditorClearAllButton} + + +
+ )} +
+ + + + + {isRunningTest ? i18nTexts.runningButton : i18nTexts.runButton} + + + {showResetModal && ( + { + resetTestOutput(); + form.reset({ defaultValue: { documents: [] } }); + setShowResetModal(false); + }} + closeModal={() => setShowResetModal(false)} + /> + )} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/tab_output.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_tabs/test_pipeline_tabs.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx index 314964f808e448..a20cde5b1fb72d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx @@ -51,11 +51,14 @@ type Action = | { type: 'updateIsExecutingPipeline'; payload: Pick; + } + | { + type: 'reset'; }; export interface TestPipelineContext { testPipelineData: TestPipelineData; - setCurrentTestPipelineData: (data: Action) => void; + testPipelineDataDispatch: (data: Action) => void; updateTestOutputPerProcessor: ( documents: Document[] | undefined, processors: DeserializeResult @@ -69,7 +72,7 @@ const DEFAULT_TEST_PIPELINE_CONTEXT = { }, isExecutingPipeline: false, }, - setCurrentTestPipelineData: () => {}, + testPipelineDataDispatch: () => {}, updateTestOutputPerProcessor: () => {}, }; @@ -122,6 +125,10 @@ export const reducer: Reducer = (state, action) => { }; } + if (action.type === 'reset') { + return DEFAULT_TEST_PIPELINE_CONTEXT.testPipelineData; + } + return state; }; @@ -193,7 +200,7 @@ export const TestPipelineContextProvider = ({ children }: { children: React.Reac diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.ts index d8fe892a2f684c..461246844a33bf 100644 --- a/x-pack/plugins/licensing/public/services/feature_usage_service.ts +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isDate from 'lodash/isDate'; +import { isDate } from 'lodash'; import type { HttpSetup, HttpStart } from 'src/core/public'; import { LicenseType } from '../../common/types'; diff --git a/x-pack/plugins/maps/public/classes/_index.scss b/x-pack/plugins/maps/public/classes/_index.scss index 29a57612552784..3fc31b4b2f66aa 100644 --- a/x-pack/plugins/maps/public/classes/_index.scss +++ b/x-pack/plugins/maps/public/classes/_index.scss @@ -1 +1,2 @@ @import 'styles/index'; +@import 'layers/index'; diff --git a/x-pack/plugins/maps/public/classes/layers/_index.scss b/x-pack/plugins/maps/public/classes/layers/_index.scss new file mode 100644 index 00000000000000..530ac2734855d1 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/_index.scss @@ -0,0 +1 @@ +@import 'layers'; diff --git a/x-pack/plugins/maps/public/classes/layers/_layers.scss b/x-pack/plugins/maps/public/classes/layers/_layers.scss new file mode 100644 index 00000000000000..54ab7d85ef170a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/_layers.scss @@ -0,0 +1,11 @@ +.mapLayersWizardIcon { + margin-top: $euiSizeS; + + &__highlight { + fill: $euiColorFullShade; + } + + &__background { + fill: $euiColorLightShade; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx index 6e806f4530df25..d87302a6a9f2e9 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx @@ -9,13 +9,14 @@ import { i18n } from '@kbn/i18n'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { LayerWizard, RenderWizardArguments } from '../layer_wizard_registry'; import { LayerTemplate } from './layer_template'; +import { ChoroplethLayerIcon } from './cloropleth_layer_icon'; export const choroplethLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.choropleth.desc', { defaultMessage: 'Shaded areas to compare statistics across boundaries', }), - icon: 'logoElasticsearch', + icon: ChoroplethLayerIcon, renderWizard: (renderWizardArguments: RenderWizardArguments) => { return ; }, diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx new file mode 100644 index 00000000000000..e0a0d450dfbffb --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const ChoroplethLayerIcon: FunctionComponent = () => ( + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts index 0eb1d2c3b222ce..278a3c0388b015 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { ReactElement } from 'react'; +import { ReactElement, FunctionComponent } from 'react'; import { LayerDescriptor } from '../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../common/constants'; @@ -28,7 +28,7 @@ export type LayerWizard = { categories: LAYER_WIZARD_CATEGORY[]; checkVisibility?: () => Promise; description: string; - icon: string; + icon: string | FunctionComponent; prerequisiteSteps?: Array<{ id: string; label: string }>; renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement; title: string; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx new file mode 100644 index 00000000000000..818ff789da19bb --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const ClustersLayerIcon: FunctionComponent = () => ( + + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index ee97fdd0a2bf68..5d0a414cd0d184 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -29,13 +29,14 @@ import { STYLE_TYPE, } from '../../../../common/constants'; import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; +import { ClustersLayerIcon } from './clusters_layer_icon'; export const clustersLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridClustersDescription', { defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', }), - icon: 'logoElasticsearch', + icon: ClustersLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx new file mode 100644 index 00000000000000..5f4835e386911c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.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, { FunctionComponent } from 'react'; + +export const HeatmapLayerIcon: FunctionComponent = () => ( + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index 92a0f1006ea439..652514a3b9d348 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -15,13 +15,14 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants'; +import { HeatmapLayerIcon } from './heatmap_layer_icon'; export const heatmapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { defaultMessage: 'Geospatial data grouped in grids to show density', }), - icon: 'logoElasticsearch', + icon: HeatmapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx new file mode 100644 index 00000000000000..ed5d38bba0fba2 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.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, { FunctionComponent } from 'react'; + +export const Point2PointLayerIcon: FunctionComponent = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index fee84d0208978f..74e690d4d3204a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -23,13 +23,14 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; +import { Point2PointLayerIcon } from './point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.pewPewDescription', { defaultMessage: 'Aggregated data paths between the source and destination', }), - icon: 'logoElasticsearch', + icon: Point2PointLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx new file mode 100644 index 00000000000000..dcd4985f442802 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.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 React, { FunctionComponent } from 'react'; + +export const EsDocumentsLayerIcon: FunctionComponent = () => ( + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 249b9a2454d7d5..af2061d6c541f7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -15,6 +15,7 @@ import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_ve import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; +import { EsDocumentsLayerIcon } from './es_documents_layer_icon'; export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); @@ -33,7 +34,7 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.esSearchDescription', { defaultMessage: 'Points, lines, and polygons from Elasticsearch', }), - icon: 'logoElasticsearch', + icon: EsDocumentsLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index 9a415ac0718b35..d808e4277f0755 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -7,3 +7,4 @@ export { SearchResponse7 } from './types/es_client'; export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; +export { composeValidators, patternValidator } from './util/validators'; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index d176c22bdbb62f..95d06e62f9ef00 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -167,6 +167,7 @@ export interface DataFrameAnalyticsExplorationQueryState { ml: { jobId: JobId; analysisType: DataFrameAnalysisConfigType; + defaultIsTraining?: boolean; }; } @@ -176,6 +177,7 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< jobId: JobId; analysisType: DataFrameAnalysisConfigType; globalState?: MlCommonGlobalState; + defaultIsTraining?: boolean; } >; diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 04a460251cb6f9..878f5a2c71cb98 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -4,18 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEmpty from 'lodash/isEmpty'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import pick from 'lodash/pick'; +import { isEmpty, isEqual, each, pick } from 'lodash'; import semver from 'semver'; import moment, { Duration } from 'moment'; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 67db378fb7951a..0527b8f6d9f609 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -10,9 +10,7 @@ * getting the annotations via props (used in Anomaly Explorer and Single Series Viewer). */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import uniq from 'lodash/uniq'; +import { uniq } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index d6eb0afed753df..0a2c67a3b0dcb8 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -9,9 +9,7 @@ */ import PropTypes from 'prop-types'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { get } from 'lodash'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 299173fc4436d2..1f8c8633afa476 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -7,9 +7,7 @@ import { EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index e0b20ab7317492..cd3875f8cbd2a0 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -11,12 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import pick from 'lodash/pick'; +import { get, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js index 505ccf46c5a625..a1ca22eb292ceb 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; +import { each } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 114dc463fa3a6d..d898734f34c931 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; +import { cloneDeep } from 'lodash'; import moment from 'moment'; import rison from 'rison-node'; import PropTypes from 'prop-types'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index ad5915b39d5210..e37efe60f80184 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -12,7 +12,6 @@ import React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import {} from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index d22bba7738db42..49f3f2311a9381 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -67,6 +67,17 @@ export const defaultSearchQuery = { match_all: {}, }; +export const getDefaultTrainingFilterQuery = (resultsField: string, isTraining: boolean) => ({ + bool: { + minimum_should_match: 1, + should: [ + { + match: { [`${resultsField}.is_training`]: isTraining }, + }, + ], + }, +}); + export interface SearchQuery { track_total_hits?: boolean; query: SavedSearchQuery; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 83eebccd310e3c..7ba3e910ddd320 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -8,6 +8,7 @@ export { getAnalysisType, getDependentVar, getPredictionFieldName, + getDefaultTrainingFilterQuery, isOutlierAnalysis, refreshAnalyticsList$, useRefreshAnalyticsList, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index 9fe59635932299..bfa63e21e6c94e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -82,7 +82,11 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => jobStats.state === DATA_FRAME_TASK_STATE.STOPPED ) { clearInterval(interval); - setJobFinished(true); + // Check job has started. Jobs that fail to start will also have STOPPED state + setJobFinished( + progressStats.currentPhase === progressStats.totalPhases && + progressStats.progress === 100 + ); } } else { clearInterval(interval); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 2e3a5d89367ce3..28ef3898cde97f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -13,20 +13,20 @@ import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; + defaultIsTraining?: boolean; } -export const ClassificationExploration: FC = ({ jobId }) => { - return ( - - ); -}; +export const ClassificationExploration: FC = ({ jobId, defaultIsTraining }) => ( + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 84b44ef0d349f3..f3fc65d264e62b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -27,9 +27,15 @@ interface Props { jobId: string; title: string; EvaluatePanel: FC; + defaultIsTraining?: boolean; } -export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { +export const ExplorationPageWrapper: FC = ({ + jobId, + title, + EvaluatePanel, + defaultIsTraining, +}) => { const { indexPattern, isInitialized, @@ -70,6 +76,7 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel needsDestIndexPattern={needsDestIndexPattern} setEvaluateSearchQuery={setSearchQuery} title={title} + defaultIsTraining={defaultIsTraining} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 8c158c1ca14a0c..8ed732bf7da2bf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -52,7 +52,7 @@ export const ExplorationQueryBar: FC = ({ if (defaultQueryString !== undefined) { setSearchInput({ query: defaultQueryString, language: SEARCH_QUERY_LANGUAGE.KUERY }); } - }, []); + }, [defaultQueryString !== undefined]); const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 07a15b01fca93f..ef014b07a937e1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -20,6 +20,7 @@ import { SEARCH_SIZE, defaultSearchQuery, getAnalysisType, + getDefaultTrainingFilterQuery, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; @@ -30,6 +31,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; import { useMlKibana } from '../../../../../contexts/kibana'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; +import { useUrlState } from '../../../../../util/url_state'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', @@ -53,6 +55,7 @@ interface Props { needsDestIndexPattern: boolean; setEvaluateSearchQuery: React.Dispatch>; title: string; + defaultIsTraining?: boolean; } export const ExplorationResultsTable: FC = React.memo( @@ -63,18 +66,36 @@ export const ExplorationResultsTable: FC = React.memo( needsDestIndexPattern, setEvaluateSearchQuery, title, + defaultIsTraining, }) => { const { services: { mlServices: { mlApiServices }, }, } = useMlKibana(); + const [globalState, setGlobalState] = useUrlState('_g'); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [defaultQueryString, setDefaultQueryString] = useState(); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); + useEffect(() => { + if (defaultIsTraining !== undefined) { + // Apply defaultIsTraining filter + setSearchQuery( + getDefaultTrainingFilterQuery(jobConfig.dest.results_field, defaultIsTraining) + ); + setDefaultQueryString(`${jobConfig.dest.results_field}.is_training : ${defaultIsTraining}`); + // Clear defaultIsTraining from url + setGlobalState('ml', { + analysisType: globalState.ml.analysisType, + jobId: globalState.ml.jobId, + }); + } + }, []); + const analysisType = getAnalysisType(jobConfig.analysis); const classificationData = useExplorationResults( @@ -140,6 +161,7 @@ export const ExplorationResultsTable: FC = React.memo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 36d91f6f41d44b..40279ecc6ffa47 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -14,17 +14,17 @@ import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; + defaultIsTraining?: boolean; } -export const RegressionExploration: FC = ({ jobId }) => { - return ( - - ); -}; +export const RegressionExploration: FC = ({ jobId, defaultIsTraining }) => ( + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index f4f01330271fce..4620bbd969fab2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -32,7 +32,8 @@ import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_fr export const Page: FC<{ jobId: string; analysisType: DataFrameAnalysisConfigType; -}> = ({ jobId, analysisType }) => ( + defaultIsTraining?: boolean; +}> = ({ jobId, analysisType, defaultIsTraining }) => ( @@ -70,10 +71,10 @@ export const Page: FC<{ )} {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( - + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index ce24892c9de454..b4efca39e200c2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -6,7 +6,7 @@ import { EuiToolTip } from '@elastic/eui'; import React, { FC } from 'react'; -import { isEqual, cloneDeep } from 'lodash'; +import { cloneDeep, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { IIndexPattern } from 'src/plugins/data/common'; import { DeepReadonly } from '../../../../../../../common/types/common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 71e503998ed47d..c2018463fdf499 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -6,12 +6,7 @@ import { useState } from 'react'; import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import sortBy from 'lodash/sortBy'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { sortBy, get } from 'lodash'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx index 0f6ffe44cc9f82..78bf07435053de 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx @@ -5,9 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import debounce from 'lodash/debounce'; +import { debounce } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts index 5e7de14f451c20..1cc513e778b2f6 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; +import { cloneDeep } from 'lodash'; import uuid from 'uuid/v4'; import { CombinedField } from './types'; import { @@ -54,7 +54,7 @@ export function addCombinedFieldsToPipeline( pipeline: IngestPipeline, combinedFields: CombinedField[] ) { - const updatedPipeline = _.cloneDeep(pipeline); + const updatedPipeline = cloneDeep(pipeline); combinedFields.forEach((combinedField) => { updatedPipeline.processors.push({ set: { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 9ca1d935c6e1e4..bf6b48fa18b47e 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -11,24 +11,7 @@ * and manages the layout of the charts in the containing div. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import find from 'lodash/find'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import sortBy from 'lodash/sortBy'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import map from 'lodash/map'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import reduce from 'lodash/reduce'; +import { get, each, find, sortBy, map, reduce } from 'lodash'; import { buildConfig } from './explorer_chart_config_builder'; import { chartLimits, getChartType } from '../../util/chart_utils'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index 8e9e8a03929c31..5e6901408422bd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; +import { cloneDeep } from 'lodash'; import mockAnomalyChartRecords from './__mocks__/mock_anomaly_chart_records.json'; import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx index f464eaf362c3a6..359dc11ca08d16 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx @@ -10,15 +10,7 @@ import React from 'react'; import './_explorer.scss'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import uniq from 'lodash/uniq'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { isEqual, uniq, get } from 'lodash'; import d3 from 'd3'; import moment from 'moment'; import DragSelect from 'dragselect'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index 08830decc9449d..c309e1f4ef8e80 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -8,7 +8,7 @@ * utils for Anomaly Explorer. */ -import { chain, get, union, uniq } from 'lodash'; +import { get, union, sortBy, uniq } from 'lodash'; import moment from 'moment-timezone'; import { @@ -279,17 +279,17 @@ export function getViewBySwimlaneOptions({ const selectedJobIds = selectedJobs.map((d) => d.id); // Unique influencers for the selected job(s). - const viewByOptions = chain( - mlJobService.jobs.reduce((reducedViewByOptions, job) => { - if (selectedJobIds.some((jobId) => jobId === job.job_id)) { - return reducedViewByOptions.concat(job.analysis_config.influencers || []); - } - return reducedViewByOptions; - }, []) - ) - .uniq() - .sortBy((fieldName) => fieldName.toLowerCase()) - .value(); + const viewByOptions = sortBy( + uniq( + mlJobService.jobs.reduce((reducedViewByOptions, job) => { + if (selectedJobIds.some((jobId) => jobId === job.job_id)) { + return reducedViewByOptions.concat(job.analysis_config.influencers || []); + } + return reducedViewByOptions; + }, []) + ), + (fieldName) => fieldName.toLowerCase() + ); viewByOptions.push(VIEW_BY_JOB_LABEL); let viewBySwimlaneOptions = viewByOptions; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 558fe4c73c597a..fb64d4767eac00 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -7,9 +7,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import sortBy from 'lodash/sortBy'; +import { sortBy } from 'lodash'; import moment from 'moment'; import { toLocaleString } from '../../../../util/string_utils'; diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index 72073dfd26a973..e633aef59297ef 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -12,27 +12,25 @@ import { i18n } from '@kbn/i18n'; -import { CoreSetup } from 'kibana/public'; -import { ManagementSetup } from 'src/plugins/management/public'; -import { MlStartDependencies } from '../../plugin'; +import type { CoreSetup } from 'kibana/public'; +import type { ManagementSetup } from 'src/plugins/management/public'; +import type { MlStartDependencies } from '../../plugin'; -import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; +import type { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; export function registerManagementSection( - management: ManagementSetup | undefined, + management: ManagementSetup, core: CoreSetup ) { - if (management !== undefined) { - return management.sections.section.insightsAndAlerting.registerApp({ - id: 'jobsListLink', - title: i18n.translate('xpack.ml.management.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', - }), - order: 2, - async mount(params: ManagementAppMountParams) { - const { mountApp } = await import('./jobs_list'); - return mountApp(core, params); - }, - }); - } + return management.sections.section.insightsAndAlerting.registerApp({ + id: 'jobsListLink', + title: i18n.translate('xpack.ml.management.jobsListTitle', { + defaultMessage: 'Machine Learning Jobs', + }), + order: 2, + async mount(params: ManagementAppMountParams) { + const { mountApp } = await import('./jobs_list'); + return mountApp(core, params); + }, + }); } diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx index 9a852c491ee27f..8219ba8731bd34 100644 --- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx +++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx @@ -40,3 +40,7 @@ export const OverviewPage: FC = () => { ); }; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default OverviewPage; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index f9f2ebe48f4aa2..b2d2a92617922d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -67,10 +67,11 @@ const PageWrapper: FC = ({ location, deps }) => { } const jobId: string = globalState.ml.jobId; const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType; + const defaultIsTraining: boolean | undefined = globalState.ml.defaultIsTraining; return ( - + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 0e07b0edfbe560..8ce51f4f5dea92 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, Suspense } from 'react'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; -import { NavigateToPath } from '../../contexts/kibana'; +import type { NavigateToPath } from '../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; -import { OverviewPage } from '../../overview'; import { checkFullLicense } from '../../license'; import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; @@ -22,6 +21,8 @@ import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; +const OverviewPage = React.lazy(() => import('../../overview/overview_page')); + export const overviewRouteFactory = ( navigateToPath: NavigateToPath, basePath: string @@ -52,7 +53,10 @@ const PageWrapper: FC = ({ deps }) => { return ( - + {/* No fallback yet, we don't show a loading spinner on an outer level until context is available either. */} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.js b/x-pack/plugins/ml/public/application/services/forecast_service.js index a99c82015df4d1..a36d57424eeb0d 100644 --- a/x-pack/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/plugins/ml/public/application/services/forecast_service.js @@ -6,15 +6,7 @@ // Service for carrying out requests to run ML forecasts and to obtain // data on forecasts that have been performed. -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import find from 'lodash/find'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; +import { get, find, each } from 'lodash'; import { map } from 'rxjs/operators'; import { ml } from './ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index eef760fb5d017e..939ad34e77a395 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -4,21 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import find from 'lodash/find'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isNumber from 'lodash/isNumber'; +import { cloneDeep, each, find, get, isNumber } from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/services/mapping_service.js b/x-pack/plugins/ml/public/application/services/mapping_service.js index 76ed4949954770..b14456c4b229c5 100644 --- a/x-pack/plugins/ml/public/application/services/mapping_service.js +++ b/x-pack/plugins/ml/public/application/services/mapping_service.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; +import { each } from 'lodash'; import { ml } from './ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 8ba71ba948b150..fd7055477a8752 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -13,12 +13,7 @@ // Returned response contains a results property containing the requested aggregation. import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { each, get } from 'lodash'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { JobId } from '../../../../common/types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index fac1ee1480172e..4e55b092de5ad5 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { each, get } from 'lodash'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../../util/string_utils'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 9f18eb1f4fed67..bf99320fdfd4a1 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -9,9 +9,7 @@ */ import PropTypes from 'prop-types'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { get } from 'lodash'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 78583fc4872b2d..1d166b7be9bc16 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -12,18 +12,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import useObservable from 'react-use/lib/useObservable'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import reduce from 'lodash/reduce'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { isEqual, reduce, each, get } from 'lodash'; import d3 from 'd3'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index b8f5f088227663..e43ba8c87083a4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -4,18 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import find from 'lodash/find'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import filter from 'lodash/filter'; +import { each, find, get, filter } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -29,7 +18,7 @@ import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; import { ModelPlotOutput } from '../services/results_service/result_service_rx'; import { Job } from '../../../common/types/anomaly_detection_jobs'; -import { EntityField } from '../..'; +import { EntityField } from '../../../common/util/anomaly_utils'; function getMetricData( job: Job, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index c8c1c98e758b88..d24794382128d5 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -10,15 +10,7 @@ * Viewer dashboard. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import each from 'lodash/each'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import find from 'lodash/find'; +import { each, get, find } from 'lodash'; import moment from 'moment-timezone'; import { isTimeSeriesViewJob } from '../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/plugins/ml/public/application/util/chart_config_builder.js index 0c4aa4f717dbe2..62e64b3d4092e5 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.js @@ -9,9 +9,7 @@ * in the source metric data. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { get } from 'lodash'; import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; 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 2586dfe45345eb..341f3a877dd86c 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -4,25 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataPublicPluginSetup } from 'src/plugins/data/public'; -import { +import type { DataPublicPluginSetup } from 'src/plugins/data/public'; +import type { IUiSettingsClient, ChromeStart, SavedObjectsClientContract, ApplicationStart, HttpStart, I18nStart, -} from 'kibana/public'; -import { IndexPatternsContract, DataPublicPluginStart } from 'src/plugins/data/public'; -import { DocLinksStart, ToastsStart, OverlayStart, ChromeRecentlyAccessed, IBasePath, } from 'kibana/public'; -import { SharePluginStart } from 'src/plugins/share/public'; -import { SecurityPluginSetup } from '../../../../security/public'; +import type { IndexPatternsContract, DataPublicPluginStart } from 'src/plugins/data/public'; +import type { SharePluginStart } from 'src/plugins/share/public'; +import type { SecurityPluginSetup } from '../../../../security/public'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.js b/x-pack/plugins/ml/public/application/util/time_buckets.js index f859b465f5de12..bfc7c8c84001a7 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/plugins/ml/public/application/util/time_buckets.js @@ -4,21 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isPlainObject from 'lodash/isPlainObject'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isString from 'lodash/isString'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import ary from 'lodash/ary'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import sortBy from 'lodash/sortBy'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import assign from 'lodash/assign'; +import { isPlainObject, isString, ary, sortBy, assign } from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index e837cabf0b4943..6e67ff1aef03da 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; -import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container'; -import { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { MlDependencies } from '../../application/app'; +import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; +import type { JobId } from '../../../common/types/anomaly_detection_jobs'; +import type { MlDependencies } from '../../application/app'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, @@ -59,15 +59,17 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ReactDOM.render( - + + + , node ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 9d2fd07e11be5a..8a977ed5820c1e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -6,15 +6,15 @@ import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor } from 'kibana/public'; +import type { StartServicesAccessor } from 'kibana/public'; -import { +import type { EmbeddableFactoryDefinition, IContainer, } from '../../../../../../src/plugins/embeddable/public'; import { HttpService } from '../../application/services/http_service'; -import { MlPluginStart, MlStartDependencies } from '../../plugin'; -import { MlDependencies } from '../../application/app'; +import type { MlPluginStart, MlStartDependencies } from '../../plugin'; +import type { MlDependencies } from '../../application/app'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, AnomalySwimlaneEmbeddableInput, diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 60681446ac7aa6..0291fa1564a2df 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -145,3 +145,7 @@ export const EmbeddableSwimLaneContainer: FC = (
); }; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default EmbeddableSwimLaneContainer; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container_lazy.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container_lazy.tsx new file mode 100644 index 00000000000000..faeb9cbb3c8f25 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container_lazy.tsx @@ -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 React from 'react'; + +export const EmbeddableSwimLaneContainer = React.lazy( + () => import('./embeddable_swim_lane_container') +); diff --git a/x-pack/plugins/ml/public/embeddables/index.ts b/x-pack/plugins/ml/public/embeddables/index.ts index cc4bec0b67836c..06cb5afa71cff5 100644 --- a/x-pack/plugins/ml/public/embeddables/index.ts +++ b/x-pack/plugins/ml/public/embeddables/index.ts @@ -5,8 +5,8 @@ */ import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane'; -import { MlCoreSetup } from '../plugin'; -import { EmbeddableSetup } from '../../../../../src/plugins/embeddable/public'; +import type { MlCoreSetup } from '../plugin'; +import type { EmbeddableSetup } from '../../../../../src/plugins/embeddable/public'; export * from './constants'; export * from './types'; diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 93ec79d9b8310f..b2979e44d0927a 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; -import { JobId } from '../../common/types/anomaly_detection_jobs'; -import { SwimlaneType } from '../application/explorer/explorer_constants'; -import { Filter } from '../../../../../src/plugins/data/common/es_query/filters'; -import { Query, RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; -import { +import type { CoreStart } from 'kibana/public'; +import type { JobId } from '../../common/types/anomaly_detection_jobs'; +import type { SwimlaneType } from '../application/explorer/explorer_constants'; +import type { Filter } from '../../../../../src/plugins/data/common/es_query/filters'; +import type { + Query, + RefreshInterval, + TimeRange, +} from '../../../../../src/plugins/data/common/query'; +import type { EmbeddableInput, EmbeddableOutput, IEmbeddable, } from '../../../../../src/plugins/embeddable/public'; -import { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; -import { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; -import { MlDependencies } from '../application/app'; -import { AppStateSelectedCells } from '../application/explorer/explorer_utils'; +import type { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; +import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; +import type { MlDependencies } from '../application/app'; +import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 80308977735d26..c43df1e1a3d2c6 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +// Be careful adding exports to this file, it may increase the bundle size of +// the ML plugin's page load bundle. You should either just export types or +// use `getMlSharedImports()` to export static code. + import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; import { MlPlugin, @@ -20,5 +24,29 @@ export const plugin: PluginInitializer< MlStartDependencies > = (initializerContext: PluginInitializerContext) => new MlPlugin(initializerContext); -export { MlPluginSetup, MlPluginStart }; -export * from './shared'; +export type { MlPluginSetup, MlPluginStart }; +export type { + AnomaliesTableRecord, + DataRecognizerConfigResponse, + Influencer, + JobExistResult, + JobStat, + MlCapabilitiesResponse, + MlSummaryJob, + UseIndexDataReturnType, + EsSorting, + RenderCellValue, +} from './shared'; + +// Static exports +export { getSeverityColor, getSeverityType } from '../common/util/anomaly_utils'; +export { ANOMALY_SEVERITY } from '../common'; + +// Bundled shared exports +// Exported this way so the code doesn't end up in ML's page load bundle +export const getMlSharedImports = async () => { + return await import('./shared'); +}; +// Helper to get Type returned by getMlSharedImports. +type AwaitReturnType = T extends PromiseLike ? U : T; +export type GetMlSharedImportsReturnType = AwaitReturnType>; diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 97ee083bedaa69..9a900c456d5164 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEmpty from 'lodash/isEmpty'; -import { +import { isEmpty } from 'lodash'; +import type { AnomalyDetectionQueryState, AnomalyDetectionUrlState, ExplorerAppState, diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts index 59dddeed298885..a03497092d3b3c 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/common.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEmpty from 'lodash/isEmpty'; +import { isEmpty } from 'lodash'; import { MlGenericUrlState } from '../../common/types/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 88761edf241a97..2408290e76773d 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -61,12 +61,13 @@ export function createDataFrameAnalyticsExplorationUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; if (mlUrlGeneratorState) { - const { jobId, analysisType, globalState } = mlUrlGeneratorState; + const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, analysisType, + defaultIsTraining, }, ...globalState, }; diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts index abec5cc2b7d1ef..704135f5546b13 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/public'; -import { +import type { CoreSetup } from 'kibana/public'; +import type { SharePluginSetup, UrlGeneratorsDefinition, UrlGeneratorState, } from '../../../../../src/plugins/share/public'; -import { MlStartDependencies } from '../plugin'; +import type { MlStartDependencies } from '../plugin'; import { ML_PAGES, ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; -import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; +import type { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; import { createAnomalyDetectionJobManagementUrl, createAnomalyDetectionCreateJobSelectType, diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 8feef489fdde1b..034ed090e2212d 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { +import type { AppMountParameters, CoreSetup, CoreStart, @@ -14,29 +14,26 @@ import { } from 'kibana/public'; import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; -import { ManagementSetup } from 'src/plugins/management/public'; -import { SharePluginSetup, SharePluginStart } from 'src/plugins/share/public'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; -import { EmbeddableSetup } from 'src/plugins/embeddable/public'; + +import type { ManagementSetup } from 'src/plugins/management/public'; +import type { SharePluginSetup, SharePluginStart } from 'src/plugins/share/public'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { HomePublicPluginSetup } from 'src/plugins/home/public'; +import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; +import type { EmbeddableSetup } from 'src/plugins/embeddable/public'; + import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; -import { SecurityPluginSetup } from '../../security/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { registerManagementSection } from './application/management'; -import { LicenseManagementUIPluginSetup } from '../../license_management/public'; -import { setDependencyCache } from './application/util/dependency_cache'; +import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import type { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; + +import type { LicenseManagementUIPluginSetup } from '../../license_management/public'; +import type { LicensingPluginSetup } from '../../licensing/public'; +import type { SecurityPluginSetup } from '../../security/public'; + import { PLUGIN_ICON_SOLUTION, PLUGIN_ID } from '../common/constants/app'; -import { registerFeature } from './register_feature'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { registerMlUiActions } from './ui_actions'; -import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { registerUrlGenerator } from './ml_url_generator'; -import { isFullLicense, isMlEnabled } from '../common/license'; -import { registerEmbeddables } from './embeddables'; + +import { setDependencyCache } from './application/util/dependency_cache'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -101,12 +98,21 @@ export class MlPlugin implements Plugin { }, }); - const managementApp = registerManagementSection(pluginsSetup.management, core); - const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(async (license) => { const [coreStart] = await core.getStartServices(); + const { + isFullLicense, + isMlEnabled, + registerEmbeddables, + registerFeature, + registerManagementSection, + registerMlUiActions, + registerUrlGenerator, + MlCardState, + } = await import('./register_helper'); + if (isMlEnabled(license)) { // add ML to home page if (pluginsSetup.home) { @@ -129,22 +135,17 @@ export class MlPlugin implements Plugin { // register various ML plugin features which require a full license if (isFullLicense(license)) { - if (canManageMLJobs && managementApp) { - managementApp.enable(); + if (canManageMLJobs && pluginsSetup.management !== undefined) { + registerManagementSection(pluginsSetup.management, core).enable(); } registerEmbeddables(pluginsSetup.embeddable, core); registerMlUiActions(pluginsSetup.uiActions, core); - } else if (managementApp) { - managementApp.disable(); } } else { // if ml is disabled in elasticsearch, disable ML in kibana this.appUpdater.next(() => ({ status: AppStatus.inaccessible, })); - if (managementApp) { - managementApp.disable(); - } } }); diff --git a/x-pack/plugins/ml/public/register_helper.ts b/x-pack/plugins/ml/public/register_helper.ts new file mode 100644 index 00000000000000..97574e296d1eb8 --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; + +export { isFullLicense, isMlEnabled } from '../common/license'; + +export { registerEmbeddables } from './embeddables'; +export { registerFeature } from './register_feature'; +export { registerManagementSection } from './application/management'; +export { registerMlUiActions } from './ui_actions'; +export { registerUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 4c27854ec719bd..af77468fa57d7d 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -6,7 +6,7 @@ import { PluginInitializerContext } from 'kibana/server'; import { MlServerPlugin } from './plugin'; -export { MlPluginSetup, MlPluginStart } from './plugin'; +export type { MlPluginSetup, MlPluginStart } from './plugin'; export * from './shared'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts index d9ebccd5547331..06577d69371019 100644 --- a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts +++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import isEmpty from 'lodash/isEmpty'; +import { isEmpty } from 'lodash'; import { ISavedObjectsRepository } from 'kibana/server'; import { getInternalRepository } from './internal_repository'; diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index 24f1d6951c9400..d45532e956f42d 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -5,8 +5,7 @@ */ import Boom from 'boom'; -import each from 'lodash/each'; -import get from 'lodash/get'; +import { each, get } from 'lodash'; import { IScopedClusterClient } from 'kibana/server'; import { ANNOTATION_EVENT_USER, ANNOTATION_TYPE } from '../../../common/constants/annotations'; diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index 1d59db8fa564f4..3edc675c06c0ee 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import cloneDeep from 'lodash/cloneDeep'; -import each from 'lodash/each'; -import remove from 'lodash/remove'; -import sortBy from 'lodash/sortBy'; -import get from 'lodash/get'; +import { cloneDeep, each, remove, sortBy, get } from 'lodash'; import { mlLog } from '../../client/log'; diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index 981ffe9618d9fa..5ff71b61bec54a 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -10,7 +10,7 @@ * And a minimum bucket span */ -import get from 'lodash/get'; +import { get } from 'lodash'; export function polledDataCheckerFactory({ asCurrentUser }) { class PolledDataChecker { diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 95c4e79150059c..7ff25c22f48960 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -5,10 +5,7 @@ */ import { IScopedClusterClient } from 'kibana/server'; -import get from 'lodash/get'; -import each from 'lodash/each'; -import last from 'lodash/last'; -import find from 'lodash/find'; +import { get, each, last, find } from 'lodash'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/server'; import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; import { getSafeAggregationName } from '../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 1be0751e15f227..2e2a9e21aa9594 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import cloneDeep from 'lodash/cloneDeep'; +import { cloneDeep } from 'lodash'; import { IScopedClusterClient } from 'kibana/server'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts index 12458af0521a9a..ddf73166e18589 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import cloneDeep from 'lodash/cloneDeep'; +import { cloneDeep } from 'lodash'; import { IScopedClusterClient } from 'kibana/server'; diff --git a/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js index 588e0e10a8d630..65820b5281338c 100644 --- a/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import sortBy from 'lodash/sortBy'; -import each from 'lodash/each'; +import { sortBy, each } from 'lodash'; import moment from 'moment-timezone'; import { diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 190b5d99309d7d..4c6f9eb4058028 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import sortBy from 'lodash/sortBy'; -import slice from 'lodash/slice'; -import get from 'lodash/get'; +import { sortBy, slice, get } from 'lodash'; import moment from 'moment'; import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; diff --git a/x-pack/plugins/ml/server/shared.ts b/x-pack/plugins/ml/server/shared.ts index 100433b23f7d13..68ee489a3b5ce4 100644 --- a/x-pack/plugins/ml/server/shared.ts +++ b/x-pack/plugins/ml/server/shared.ts @@ -7,5 +7,5 @@ export * from '../common/types/anomalies'; export * from '../common/types/anomaly_detection_jobs'; export * from './lib/capabilities/errors'; -export { ModuleSetupPayload } from './shared_services/providers/modules'; +export type { ModuleSetupPayload } from './shared_services/providers/modules'; export { getHistogramsForFields } from './models/data_visualizer/'; diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json index b097b0432e75d1..173514565c8bba 100644 --- a/x-pack/plugins/security_solution/cypress/cypress.json +++ b/x-pack/plugins/security_solution/cypress/cypress.json @@ -1,6 +1,9 @@ { "baseUrl": "http://localhost:5601", "defaultCommandTimeout": 120000, + "retries": { + "runMode": 2 + }, "screenshotsFolder": "../../../target/kibana-security-solution/cypress/screenshots", "trashAssetsBeforeRuns": false, "video": false, diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 7f02d41ad1b0ca..db841d2a732c48 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -158,6 +158,7 @@ describe('Alerts', () => { }); }); }); + context('Opening alerts', () => { beforeEach(() => { esArchiverLoad('closed_alerts'); @@ -204,6 +205,7 @@ describe('Alerts', () => { }); }); }); + context('Marking alerts as in-progress', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts index 20cf624b3360d8..3fa304ab7cf190 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts @@ -43,6 +43,7 @@ describe('Alerts detection rules', () => { waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + cy.get(RULE_NAME) .eq(FIFTH_RULE) .invoke('text') @@ -56,7 +57,6 @@ describe('Alerts detection rules', () => { activateRule(SEVENTH_RULE); waitForRuleToBeActivated(); sortByActivatedRules(); - cy.get(RULE_NAME) .eq(FIRST_RULE) .invoke('text') @@ -70,7 +70,6 @@ describe('Alerts detection rules', () => { cy.wrap(expectedRulesNames).should('include', seventhRuleName); }); }); - cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch'); cy.get(RULE_SWITCH).eq(SECOND_RULE).should('have.attr', 'role', 'switch'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 17ff1dad799606..f999c5cecc3923 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule, existingRule } from '../objects/rule'; +import { newRule, existingRule, indexPatterns, editedRule } from '../objects/rule'; +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, @@ -12,20 +20,49 @@ import { RULE_NAME, RULES_ROW, RULES_TABLE, + RULE_SWITCH, SEVERITY, SHOWING_RULES_TEXT, } from '../screens/alerts_detection_rules'; import { + ABOUT_CONTINUE_BTN, + ABOUT_EDIT_BUTTON, + ACTIONS_THROTTLE_INPUT, + CUSTOM_QUERY_INPUT, + DEFINE_CONTINUE_BUTTON, + DEFINE_EDIT_BUTTON, + DEFINE_INDEX_INPUT, + RISK_INPUT, + RULE_DESCRIPTION_INPUT, + RULE_NAME_INPUT, + SCHEDULE_INTERVAL_AMOUNT_INPUT, + SCHEDULE_INTERVAL_UNITS_INPUT, + SEVERITY_DROPDOWN, + TAGS_FIELD, +} from '../screens/create_new_rule'; +import { + ADDITIONAL_LOOK_BACK_DETAILS, + ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -37,46 +74,46 @@ import { changeToThreeHundredRowsPerPage, deleteFirstRule, deleteSelectedRules, + editFirstRule, filterByCustomRules, goToCreateNewRule, goToRuleDetails, selectNumberOfRules, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRulesToBeLoaded, - editFirstRule, } from '../tasks/alerts_detection_rules'; import { createAndActivateRule, + fillAboutRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillScheduleRuleAndContinue, goToAboutStepTab, - goToScheduleStepTab, goToActionsStepTab, - fillAboutRule, + goToScheduleStepTab, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; +import { saveEditedRule } from '../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; -import { - ACTIONS_THROTTLE_INPUT, - CUSTOM_QUERY_INPUT, - DEFINE_INDEX_INPUT, - RULE_NAME_INPUT, - RULE_DESCRIPTION_INPUT, - TAGS_FIELD, - SEVERITY_DROPDOWN, - RISK_INPUT, - SCHEDULE_INTERVAL_AMOUNT_INPUT, - SCHEDULE_INTERVAL_UNITS_INPUT, - DEFINE_EDIT_BUTTON, - DEFINE_CONTINUE_BUTTON, - ABOUT_EDIT_BUTTON, - ABOUT_CONTINUE_BTN, -} from '../screens/create_new_rule'; -import { saveEditedRule } from '../tasks/edit_rule'; -describe('Detection rules, custom', () => { +const expectedUrls = newRule.referenceUrls.join(''); +const expectedFalsePositives = newRule.falsePositivesExamples.join(''); +const expectedTags = newRule.tags.join(''); +const expectedMitre = newRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; +const expectedEditedtags = editedRule.tags.join(''); +const expectedEditedIndexPatterns = + editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns; + +describe('Custom detection rules creation', () => { before(() => { esArchiverLoad('timeline'); }); @@ -85,7 +122,7 @@ describe('Detection rules, custom', () => { esArchiverUnload('timeline'); }); - it('Creates and activates a new custom rule', () => { + it('Creates and activates a new rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); @@ -94,27 +131,27 @@ describe('Detection rules, custom', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); + fillScheduleRuleAndContinue(newRule); // expect define step to repopulate cy.get(DEFINE_EDIT_BUTTON).click(); - cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', newRule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.text', newRule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); // expect about step to populate cy.get(ABOUT_EDIT_BUTTON).click(); - cy.get(RULE_NAME_INPUT).invoke('val').should('eq', newRule.name); + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', newRule.name); cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); cy.get(ABOUT_CONTINUE_BTN).should('not.exist'); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -124,78 +161,59 @@ describe('Detection rules, custom', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newRule.name); + cy.get(RISK_SCORE).should('have.text', newRule.riskScore); + cy.get(SEVERITY).should('have.text', newRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newRule.description); + cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description); cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', newRule.severity); - getDescriptionForTitle('Risk score').invoke('text').should('eql', newRule.riskScore); - getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); - getDescriptionForTitle('False positive examples') - .invoke('text') - .should('eql', expectedFalsePositives); - getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Index patterns') - .invoke('text') - .should('eql', expectedIndexPatterns.join('')); - getDescriptionForTitle('Custom query') - .invoke('text') - .should('eql', `${newRule.customQuery} `); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newRule.runsEvery.interval}${newRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newRule.lookBack.interval}${newRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', newRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', newRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newRule.riskScore); }); }); -describe('Deletes custom rules', () => { +describe('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); @@ -208,143 +226,132 @@ describe('Deletes custom rules', () => { esArchiverUnload('custom_rules'); }); - it('Deletes one rule', () => { - cy.get(RULES_TABLE) - .find(RULES_ROW) - .then((rules) => { - const initialNumberOfRules = rules.length; - const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1; - - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${initialNumberOfRules} rules`); - - deleteFirstRule(); - waitForRulesToBeLoaded(); - - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); - }); - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rules`); - cy.get(CUSTOM_RULES_BTN) - .invoke('text') - .should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`); - }); - }); - - it('Deletes more than one rule', () => { - cy.get(RULES_TABLE) - .find(RULES_ROW) - .then((rules) => { - const initialNumberOfRules = rules.length; - const numberOfRulesToBeDeleted = 3; - const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - numberOfRulesToBeDeleted; - - selectNumberOfRules(numberOfRulesToBeDeleted); - deleteSelectedRules(); - waitForRulesToBeLoaded(); - - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); + context('Deletion', () => { + it('Deletes one rule', () => { + cy.get(RULES_TABLE) + .find(RULES_ROW) + .then((rules) => { + const initialNumberOfRules = rules.length; + const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1; + + cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`); + + deleteFirstRule(); + waitForRulesToBeLoaded(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should( + 'eql', + expectedNumberOfRulesAfterDeletion + ); + }); + cy.get(SHOWING_RULES_TEXT).should( + 'have.text', + `Showing ${expectedNumberOfRulesAfterDeletion} rules` + ); + cy.get(CUSTOM_RULES_BTN).should( + 'have.text', + `Custom rules (${expectedNumberOfRulesAfterDeletion})` + ); }); - cy.get(SHOWING_RULES_TEXT) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rule`); - cy.get(CUSTOM_RULES_BTN) - .invoke('text') - .should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`); - }); - }); - - it('Allows a rule to be edited', () => { - editFirstRule(); - - // expect define step to populate - cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', existingRule.customQuery); - if (existingRule.index && existingRule.index.length > 0) { - cy.get(DEFINE_INDEX_INPUT).invoke('text').should('eq', existingRule.index.join('')); - } - - goToAboutStepTab(); - - // expect about step to populate - cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name); - cy.get(RULE_DESCRIPTION_INPUT).invoke('text').should('eql', existingRule.description); - cy.get(TAGS_FIELD).invoke('text').should('eql', existingRule.tags.join('')); - - cy.get(SEVERITY_DROPDOWN).invoke('text').should('eql', existingRule.severity); - cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore); - - goToScheduleStepTab(); - - // expect schedule step to populate - const intervalParts = existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g); - if (intervalParts) { - const [amount, unit] = intervalParts; - cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount); - cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit); - } else { - throw new Error('Cannot assert scheduling info on a rule without an interval'); - } - - goToActionsStepTab(); - - cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); - - goToAboutStepTab(); - - const editedRule = { - ...existingRule, - severity: 'Medium', - description: 'Edited Rule description', - }; - - fillAboutRule(editedRule); - saveEditedRule(); - - const expectedTags = editedRule.tags.join(''); - const expectedIndexPatterns = - editedRule.index && editedRule.index.length - ? editedRule.index - : [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${editedRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', editedRule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', editedRule.severity); - getDescriptionForTitle('Risk score').invoke('text').should('eql', editedRule.riskScore); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', editedRule.note); - - cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Index patterns') - .invoke('text') - .should('eql', expectedIndexPatterns.join('')); - getDescriptionForTitle('Custom query') - .invoke('text') - .should('eql', `${editedRule.customQuery} `); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + it('Deletes more than one rule', () => { + cy.get(RULES_TABLE) + .find(RULES_ROW) + .then((rules) => { + const initialNumberOfRules = rules.length; + const numberOfRulesToBeDeleted = 3; + const expectedNumberOfRulesAfterDeletion = + initialNumberOfRules - numberOfRulesToBeDeleted; + + selectNumberOfRules(numberOfRulesToBeDeleted); + deleteSelectedRules(); + waitForRulesToBeLoaded(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should( + 'eql', + expectedNumberOfRulesAfterDeletion + ); + }); + cy.get(SHOWING_RULES_TEXT).should( + 'have.text', + `Showing ${expectedNumberOfRulesAfterDeletion} rule` + ); + cy.get(CUSTOM_RULES_BTN).should( + 'have.text', + `Custom rules (${expectedNumberOfRulesAfterDeletion})` + ); + }); }); + }); - if (editedRule.interval) { - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', editedRule.interval); + context('Edition', () => { + it('Allows a rule to be edited', () => { + editFirstRule(); + + // expect define step to populate + cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery); + if (existingRule.index && existingRule.index.length > 0) { + cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join('')); + } + + goToAboutStepTab(); + + // expect about step to populate + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name); + cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description); + cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join('')); + cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity); + cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore); + + goToScheduleStepTab(); + + // expect schedule step to populate + const intervalParts = + existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g); + if (intervalParts) { + const [amount, unit] = intervalParts; + cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount); + cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit); + } else { + throw new Error('Cannot assert scheduling info on a rule without an interval'); + } + + goToActionsStepTab(); + + cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); + + goToAboutStepTab(); + fillAboutRule(editedRule); + saveEditedRule(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore); + getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags); + }); + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE) + .eq(INVESTIGATION_NOTES_TOGGLE) + .click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should( + 'have.text', + expectedEditedIndexPatterns.join('') + ); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - } + if (editedRule.interval) { + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should('have.text', editedRule.interval); + }); + } + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 76871929fe0502..e2ff51dd544a2d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { eqlRule } from '../objects/rule'; +import { eqlRule, indexPatterns } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -12,19 +12,32 @@ import { RULE_NAME, RULES_ROW, RULES_TABLE, + RULE_SWITCH, SEVERITY, } from '../screens/alerts_detection_rules'; import { ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, DEFINITION_DETAILS, - getDescriptionForTitle, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -43,14 +56,25 @@ import { import { createAndActivateRule, fillAboutRuleAndContinue, - selectEqlRuleType, fillDefineEqlRuleAndContinue, + fillScheduleRuleAndContinue, + selectEqlRuleType, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = eqlRule.referenceUrls.join(''); +const expectedFalsePositives = eqlRule.falsePositivesExamples.join(''); +const expectedTags = eqlRule.tags.join(''); +const expectedMitre = eqlRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; + describe('Detection rules, EQL', () => { before(() => { esArchiverLoad('timeline'); @@ -70,14 +94,14 @@ describe('Detection rules, EQL', () => { selectEqlRuleType(); fillDefineEqlRuleAndContinue(eqlRule); fillAboutRuleAndContinue(eqlRule); + fillScheduleRuleAndContinue(eqlRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -87,73 +111,40 @@ describe('Detection rules, EQL', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', eqlRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', eqlRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', eqlRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', eqlRule.name); + cy.get(RISK_SCORE).should('have.text', eqlRule.riskScore); + cy.get(SEVERITY).should('have.text', eqlRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - eqlRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - eqlRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - eqlRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - eqlRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${eqlRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', eqlRule.description); + cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description); cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', eqlRule.severity); - getDescriptionForTitle('Risk score').invoke('text').should('eql', eqlRule.riskScore); - getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); - getDescriptionForTitle('False positive examples') - .invoke('text') - .should('eql', expectedFalsePositives); - getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', eqlRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Index patterns') - .invoke('text') - .should('eql', expectedIndexPatterns.join('')); - getDescriptionForTitle('Custom query') - .invoke('text') - .should('eql', `${eqlRule.customQuery} `); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Event Correlation'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${eqlRule.runsEvery.interval}${eqlRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${eqlRule.lookBack.interval}${eqlRule.lookBack.type}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 47e49d48e2aec2..49ec6381cbc892 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -16,14 +16,25 @@ import { SEVERITY, } from '../screens/alerts_detection_rules'; import { + ABOUT_DETAILS, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + ANOMALY_SCORE_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, MACHINE_LEARNING_JOB_ID, MACHINE_LEARNING_JOB_STATUS, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -43,6 +54,7 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineMachineLearningRuleAndContinue, + fillScheduleRuleAndContinue, selectMachineLearningRuleType, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; @@ -50,6 +62,16 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = machineLearningRule.referenceUrls.join(''); +const expectedFalsePositives = machineLearningRule.falsePositivesExamples.join(''); +const expectedTags = machineLearningRule.tags.join(''); +const expectedMitre = machineLearningRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; + describe('Detection rules, machine learning', () => { before(() => { esArchiverLoad('prebuilt_rules_loaded'); @@ -69,6 +91,7 @@ describe('Detection rules, machine learning', () => { selectMachineLearningRuleType(); fillDefineMachineLearningRuleAndContinue(machineLearningRule); fillAboutRuleAndContinue(machineLearningRule); + fillScheduleRuleAndContinue(machineLearningRule); createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); @@ -76,7 +99,6 @@ describe('Detection rules, machine learning', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); @@ -86,67 +108,42 @@ describe('Detection rules, machine learning', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', machineLearningRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', machineLearningRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', machineLearningRule.severity); + cy.get(RULE_NAME).should('have.text', machineLearningRule.name); + cy.get(RISK_SCORE).should('have.text', machineLearningRule.riskScore); + cy.get(SEVERITY).should('have.text', machineLearningRule.severity); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - machineLearningRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - machineLearningRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - machineLearningRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - machineLearningRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${machineLearningRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', machineLearningRule.description); + cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', machineLearningRule.severity); - getDescriptionForTitle('Risk score') - .invoke('text') - .should('eql', machineLearningRule.riskScore); - getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); - getDescriptionForTitle('False positive examples') - .invoke('text') - .should('eql', expectedFalsePositives); - getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Anomaly score') - .invoke('text') - .should('eql', machineLearningRule.anomalyScoreThreshold); - getDescriptionForTitle('Anomaly score') - .invoke('text') - .should('eql', machineLearningRule.anomalyScoreThreshold); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Machine Learning'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); - cy.get(MACHINE_LEARNING_JOB_STATUS).invoke('text').should('eql', 'Stopped'); - cy.get(MACHINE_LEARNING_JOB_ID) - .invoke('text') - .should('eql', machineLearningRule.machineLearningJob); + getDetails(ANOMALY_SCORE_DETAILS).should( + 'have.text', + machineLearningRule.anomalyScoreThreshold + ); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'Stopped'); + cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningRule.machineLearningJob); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${machineLearningRule.runsEvery.interval}${machineLearningRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${machineLearningRule.lookBack.interval}${machineLearningRule.lookBack.type}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index 4edf5e18660875..090012de725343 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -4,33 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newOverrideRule } from '../objects/rule'; +import { indexPatterns, newOverrideRule, severitiesOverride } from '../objects/rule'; +import { + NUMBER_OF_ALERTS, + ALERT_RULE_NAME, + ALERT_RULE_METHOD, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, RISK_SCORE, RULE_NAME, + RULE_SWITCH, RULES_ROW, RULES_TABLE, SEVERITY, } from '../screens/alerts_detection_rules'; import { ABOUT_INVESTIGATION_NOTES, + ABOUT_DETAILS, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + DETAILS_DESCRIPTION, + DETAILS_TITLE, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, + RISK_SCORE_OVERRIDE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - ABOUT_DETAILS, - getDescriptionForTitle, - DEFINITION_DETAILS, + RULE_NAME_OVERRIDE_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, - DETAILS_TITLE, - DETAILS_DESCRIPTION, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, + TIMESTAMP_OVERRIDE_DETAILS, } from '../screens/rule_details'; import { goToManageAlertsDetectionRules, + sortRiskScore, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../tasks/alerts'; @@ -46,12 +71,24 @@ import { createAndActivateRule, fillAboutRuleWithOverrideAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillScheduleRuleAndContinue, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = newOverrideRule.referenceUrls.join(''); +const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); +const expectedTags = newOverrideRule.tags.join(''); +const expectedMitre = newOverrideRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); + describe('Detection rules, override', () => { before(() => { esArchiverLoad('timeline'); @@ -70,9 +107,10 @@ describe('Detection rules, override', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newOverrideRule); fillAboutRuleWithOverrideAndContinue(newOverrideRule); + fillScheduleRuleAndContinue(newOverrideRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -87,98 +125,75 @@ describe('Detection rules, override', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newOverrideRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newOverrideRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newOverrideRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newOverrideRule.name); + cy.get(RISK_SCORE).should('have.text', newOverrideRule.riskScore); + cy.get(SEVERITY).should('have.text', newOverrideRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newOverrideRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newOverrideRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newOverrideRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newOverrideRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newOverrideRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newOverrideRule.description); - - const expectedOverrideSeverities = ['Low', 'Medium', 'High', 'Critical']; - + cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description); cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', newOverrideRule.severity); - getDescriptionForTitle('Risk score').invoke('text').should('eql', newOverrideRule.riskScore); - getDescriptionForTitle('Risk score override') - .invoke('text') - .should('eql', `${newOverrideRule.riskOverride}signal.rule.risk_score`); - getDescriptionForTitle('Rule name override') - .invoke('text') - .should('eql', newOverrideRule.nameOverride); - getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); - getDescriptionForTitle('False positive examples') - .invoke('text') - .should('eql', expectedFalsePositives); - getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); - getDescriptionForTitle('Timestamp override') - .invoke('text') - .should('eql', newOverrideRule.timestampOverride); + getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newOverrideRule.riskScore); + getDetails(RISK_SCORE_OVERRIDE_DETAILS).should( + 'have.text', + `${newOverrideRule.riskOverride}signal.rule.risk_score` + ); + getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', newOverrideRule.nameOverride); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', newOverrideRule.timestampOverride); cy.contains(DETAILS_TITLE, 'Severity override') .invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings .then((severityOverrideIndex) => { newOverrideRule.severityOverride.forEach((severity, i) => { cy.get(DETAILS_DESCRIPTION) .eq(severityOverrideIndex + i) - .invoke('text') .should( - 'eql', - `${severity.sourceField}:${severity.sourceValue}${expectedOverrideSeverities[i]}` + 'have.text', + `${severity.sourceField}:${severity.sourceValue}${severitiesOverride[i]}` ); }); }); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Index patterns') - .invoke('text') - .should('eql', expectedIndexPatterns.join('')); - getDescriptionForTitle('Custom query') - .invoke('text') - .should('eql', `${newOverrideRule.customQuery} `); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newOverrideRule.runsEvery.interval}${newOverrideRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newOverrideRule.lookBack.interval}${newOverrideRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat'); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical'); + + sortRiskScore(); + + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', '80'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts index 986a7c7177a798..6088a9dedbd06c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts @@ -56,7 +56,7 @@ describe('Alerts rules, prebuilt rules', () => { loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText); + cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -81,7 +81,7 @@ describe('Deleting prebuilt rules', () => { loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText); + cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -113,16 +113,15 @@ describe('Deleting prebuilt rules', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterDeletion})` + ); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); }); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); - cy.get(RELOAD_PREBUILT_RULES_BTN) - .invoke('text') - .should('eql', 'Install 1 Elastic prebuilt rule '); + cy.get(RELOAD_PREBUILT_RULES_BTN).should('have.text', 'Install 1 Elastic prebuilt rule '); reloadDeletedRules(); @@ -135,9 +134,10 @@ describe('Deleting prebuilt rules', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering); }); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterRecovering})` + ); }); it('Deletes and recovers more than one rule', () => { @@ -152,12 +152,14 @@ describe('Deleting prebuilt rules', () => { waitForRulesToBeLoaded(); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); - cy.get(RELOAD_PREBUILT_RULES_BTN) - .invoke('text') - .should('eql', `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`); + cy.get(RELOAD_PREBUILT_RULES_BTN).should( + 'have.text', + `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules ` + ); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterDeletion})` + ); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion); }); @@ -173,8 +175,9 @@ describe('Deleting prebuilt rules', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering); }); - cy.get(ELASTIC_RULES_BTN) - .invoke('text') - .should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`); + cy.get(ELASTIC_RULES_BTN).should( + 'have.text', + `Elastic rules (${expectedNumberOfRulesAfterRecovering})` + ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 00175ed3baeb85..5ee7e69e877e30 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -4,27 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newThresholdRule } from '../objects/rule'; +import { indexPatterns, newThresholdRule } from '../objects/rule'; +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; import { CUSTOM_RULES_BTN, RISK_SCORE, RULE_NAME, + RULE_SWITCH, RULES_ROW, RULES_TABLE, SEVERITY, } from '../screens/alerts_detection_rules'; import { + ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + FALSE_POSITIVES_DETAILS, + DEFINITION_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - getDescriptionForTitle, - ABOUT_DETAILS, - DEFINITION_DETAILS, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + THRESHOLD_DETAILS, + TIMELINE_TEMPLATE_DETAILS, } from '../screens/rule_details'; import { @@ -44,13 +66,25 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineThresholdRuleAndContinue, + fillScheduleRuleAndContinue, selectThresholdRuleType, + waitForTheRuleToBeExecuted, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; +const expectedUrls = newThresholdRule.referenceUrls.join(''); +const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); +const expectedTags = newThresholdRule.tags.join(''); +const expectedMitre = newThresholdRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); + describe('Detection rules, threshold', () => { before(() => { esArchiverLoad('timeline'); @@ -70,9 +104,10 @@ describe('Detection rules, threshold', () => { selectThresholdRuleType(); fillDefineThresholdRuleAndContinue(newThresholdRule); fillAboutRuleAndContinue(newThresholdRule); + fillScheduleRuleAndContinue(newThresholdRule); createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); @@ -87,79 +122,60 @@ describe('Detection rules, threshold', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); - cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); - cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); - cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + cy.get(RULE_NAME).should('have.text', newThresholdRule.name); + cy.get(RISK_SCORE).should('have.text', newThresholdRule.riskScore); + cy.get(SEVERITY).should('have.text', newThresholdRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - let expectedUrls = ''; - newThresholdRule.referenceUrls.forEach((url) => { - expectedUrls = expectedUrls + url; - }); - let expectedFalsePositives = ''; - newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - let expectedTags = ''; - newThresholdRule.tags.forEach((tag) => { - expectedTags = expectedTags + tag; - }); - let expectedMitre = ''; - newThresholdRule.mitre.forEach((mitre) => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach((technique) => { - expectedMitre = expectedMitre + technique; - }); - }); - const expectedIndexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ]; - - cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); - - cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); + cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description); cy.get(ABOUT_DETAILS).within(() => { - getDescriptionForTitle('Severity').invoke('text').should('eql', newThresholdRule.severity); - getDescriptionForTitle('Risk score').invoke('text').should('eql', newThresholdRule.riskScore); - getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); - getDescriptionForTitle('False positive examples') - .invoke('text') - .should('eql', expectedFalsePositives); - getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); - getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThresholdRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDescriptionForTitle('Index patterns') - .invoke('text') - .should('eql', expectedIndexPatterns.join('')); - getDescriptionForTitle('Custom query') - .invoke('text') - .should('eql', `${newThresholdRule.customQuery} `); - getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Threshold'); - getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); - getDescriptionForTitle('Threshold') - .invoke('text') - .should( - 'eql', - `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` - ); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(THRESHOLD_DETAILS).should( + 'have.text', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); - getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThresholdRule.runsEvery.interval}${newThresholdRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThresholdRule.lookBack.interval}${newThresholdRule.lookBack.type}` + ); }); + + refreshPage(); + waitForTheRuleToBeExecuted(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .then((numberOfAlertsText) => { + cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.below', 100); + }); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThresholdRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThresholdRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThresholdRule.riskScore); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 2fed23755963b9..31d8e4666d91de 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -35,7 +35,7 @@ describe('Alerts timeline', () => { .invoke('text') .then((eventId) => { investigateFirstAlertInTimeline(); - cy.get(PROVIDER_BADGE).invoke('text').should('eql', `_id: "${eventId}"`); + cy.get(PROVIDER_BADGE).should('have.text', `_id: "${eventId}"`); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 2588c580dedd34..906fba28a7721c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -14,7 +14,7 @@ import { import { HOSTS_NAMES } from '../screens/hosts/all_hosts'; import { ANOMALIES_TAB } from '../screens/hosts/main'; import { BREADCRUMBS, HOSTS, KQL_INPUT, NETWORK } from '../screens/security_header'; -import { SERVER_SIDE_EVENT_COUNT, TIMELINE_TITLE } from '../screens/timeline'; +import { TIMELINE_TITLE } from '../screens/timeline'; import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { @@ -32,16 +32,18 @@ import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/security_header'; import { openTimelineUsingToggle } from '../tasks/security_main'; import { - addDescriptionToTimeline, addNameToTimeline, closeTimeline, - executeTimelineKQL, + populateTimeline, waitForTimelineChanges, } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../urls/state'; +import { timeline } from '../objects/timeline'; +import { TIMELINE } from '../screens/create_new_case'; + const ABSOLUTE_DATE = { endTime: '2019-08-01T20:33:29.186Z', endTimeTimeline: '2019-08-02T21:03:29.186Z', @@ -51,8 +53,7 @@ const ABSOLUTE_DATE = { startTimeTimeline: '2019-08-02T20:03:29.186Z', }; -// FLAKY: https://github.com/elastic/kibana/issues/61612 -describe.skip('url state', () => { +describe('url state', () => { it('sets the global start and end dates from the url', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( @@ -222,23 +223,12 @@ describe.skip('url state', () => { it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); openTimelineUsingToggle(); - executeTimelineKQL('host.name: *'); - - cy.get(SERVER_SIDE_EVENT_COUNT) - .invoke('text') - .then((strCount) => { - const intCount = +strCount; - cy.wrap(intCount).should('be.above', 0); - }); + populateTimeline(); cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); - const timelineName = 'Security'; - const timelineDescription = 'This is the best timeline of the world'; - addNameToTimeline(timelineName); - waitForTimelineChanges(); - addDescriptionToTimeline(timelineDescription); + addNameToTimeline(timeline.title); waitForTimelineChanges(); cy.wait('@timeline').then((response) => { @@ -249,9 +239,10 @@ describe.skip('url state', () => { cy.visit('/app/home'); cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('exist'); - cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).invoke('text').should('not.equal', 'Updating'); + cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('not.have.text', 'Updating'); + cy.get(TIMELINE).should('be.visible'); cy.get(TIMELINE_TITLE).should('be.visible'); - cy.get(TIMELINE_TITLE).should('have.attr', 'value', timelineName); + cy.get(TIMELINE_TITLE).should('have.attr', 'value', timeline.title); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 2a5c60815f450d..e84e2b7b1669f1 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -23,6 +23,12 @@ interface SeverityOverride { sourceValue: string; } +interface Interval { + interval: string; + timeType: string; + type: string; +} + export interface CustomRule { customQuery: string; name: string; @@ -38,6 +44,8 @@ export interface CustomRule { mitre: Mitre[]; note: string; timelineId: string; + runsEvery: Interval; + lookBack: Interval; } export interface ThresholdRule extends CustomRule { @@ -65,6 +73,8 @@ export interface MachineLearningRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; + runsEvery: Interval; + lookBack: Interval; } const mitre1: Mitre = { @@ -83,8 +93,8 @@ const severityOverride1: SeverityOverride = { }; const severityOverride2: SeverityOverride = { - sourceField: 'agent.type', - sourceValue: 'endpoint', + sourceField: '@timestamp', + sourceValue: '10/02/2020', }; const severityOverride3: SeverityOverride = { @@ -93,8 +103,20 @@ const severityOverride3: SeverityOverride = { }; const severityOverride4: SeverityOverride = { - sourceField: '@timestamp', - sourceValue: '10/02/2020', + sourceField: 'agent.type', + sourceValue: 'auditbeat', +}; + +const runsEvery: Interval = { + interval: '1', + timeType: 'Seconds', + type: 's', +}; + +const lookBack: Interval = { + interval: '17520', + timeType: 'Hours', + type: 'h', }; export const newRule: CustomRule = { @@ -109,6 +131,8 @@ export const newRule: CustomRule = { mitre: [mitre1, mitre2], note: '# test markdown', timelineId: '0162c130-78be-11ea-9718-118a926974a4', + runsEvery, + lookBack, }; export const existingRule: CustomRule = { @@ -132,6 +156,8 @@ export const existingRule: CustomRule = { mitre: [], note: 'This is my note', timelineId: '', + runsEvery, + lookBack, }; export const newOverrideRule: OverrideRule = { @@ -150,6 +176,8 @@ export const newOverrideRule: OverrideRule = { riskOverride: 'destination.port', nameOverride: 'agent.type', timestampOverride: '@timestamp', + runsEvery, + lookBack, }; export const newThresholdRule: ThresholdRule = { @@ -166,6 +194,8 @@ export const newThresholdRule: ThresholdRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', thresholdField: 'host.name', threshold: '10', + runsEvery, + lookBack, }; export const machineLearningRule: MachineLearningRule = { @@ -180,6 +210,8 @@ export const machineLearningRule: MachineLearningRule = { falsePositivesExamples: ['False1'], mitre: [mitre1], note: '# test markdown', + runsEvery, + lookBack, }; export const eqlRule: CustomRule = { @@ -194,4 +226,24 @@ export const eqlRule: CustomRule = { mitre: [mitre1, mitre2], note: '# test markdown', timelineId: '0162c130-78be-11ea-9718-118a926974a4', + runsEvery, + lookBack, +}; + +export const indexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; + +export const editedRule = { + ...existingRule, + severity: 'Medium', + description: 'Edited Rule description', }; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index fb7e7e73986b9a..ed05874bd4c4d8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -4,45 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; +export const ALERTS = '[data-test-subj="event"]'; -export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; +export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; -export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; +export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; -export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; +export const ALERT_RISK_SCORE_HEADER = '[data-test-subj="header-text-signal.rule.risk_score"]'; -export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; +export const ALERT_RULE_METHOD = '[data-test-subj="draggable-content-signal.rule.type"]'; -export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; +export const ALERT_RULE_NAME = '[data-test-subj="draggable-content-signal.rule.name"]'; -export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; +export const ALERT_RULE_RISK_SCORE = '[data-test-subj="draggable-content-signal.rule.risk_score"]'; -export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]'; +export const ALERT_RULE_SEVERITY = '[data-test-subj="draggable-content-signal.rule.severity"]'; -export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; +export const ALERT_RULE_VERSION = '[data-test-subj="draggable-content-signal.rule.version"]'; -export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]'; +export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; -export const ALERTS = '[data-test-subj="event"]'; +export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]'; -export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; +export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; -export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; +export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; -export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button'; +export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; -export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; +export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; -export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]'; +export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; -export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]'; +export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]'; export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN = '[data-test-subj="markSelectedAlertsInProgressButton"]'; +export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; + export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; -export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; +export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]'; -export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]'; +export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; + +export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]'; + +export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; + +export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]'; + +export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button'; + +export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index dda371126d5aa6..e1ab5ff30572f8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -57,6 +57,12 @@ export const INVESTIGATION_NOTES_TEXTAREA = export const FALSE_POSITIVES_INPUT = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input'; +export const LOOK_BACK_INTERVAL = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="interval"]'; + +export const LOOK_BACK_TIME_TYPE = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]'; + export const MACHINE_LEARNING_DROPDOWN = '[data-test-subj="mlJobSelect"] button'; export const MACHINE_LEARNING_LIST = '.euiContextMenuItem__text'; @@ -73,6 +79,8 @@ export const MITRE_TECHNIQUES_INPUT = export const REFERENCE_URLS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input'; +export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]'; + export const RISK_INPUT = '.euiRangeInput'; export const RISK_MAPPING_OVERRIDE_OPTION = '#risk_score-mapping-override'; @@ -88,21 +96,29 @@ export const RULE_NAME_INPUT = export const RULE_NAME_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleRuleNameOverride"]'; +export const RULE_STATUS = '[data-test-subj="ruleStatus"]'; + export const RULE_TIMESTAMP_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleTimestampOverride"]'; +export const RUNS_EVERY_INTERVAL = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]'; + +export const RUNS_EVERY_TIME_TYPE = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]'; + export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; export const SCHEDULE_EDIT_TAB = '[data-test-subj="edit-rule-schedule-tab"]'; export const SCHEDULE_INTERVAL_AMOUNT_INPUT = - '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-amount-input"]'; + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]'; export const SCHEDULE_INTERVAL_UNITS_INPUT = - '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-units-input"]'; + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]'; export const SCHEDULE_LOOKBACK_AMOUNT_INPUT = - '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-amount-input"]'; + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]'; export const SCHEDULE_LOOKBACK_UNITS_INPUT = '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-units-input"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 98fc7b06a99083..5a376e95e38dd3 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const getDescriptionForTitle = (title: string) => - cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); - -export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; -export const DETAILS_TITLE = '.euiDescriptionList__title'; - export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; @@ -17,9 +11,23 @@ export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggl export const ABOUT_DETAILS = '[data-test-subj="aboutRule"] [data-test-subj="listItemColumnStepRuleDescription"]'; +export const ADDITIONAL_LOOK_BACK_DETAILS = 'Additional look-back time'; + +export const ANOMALY_SCORE_DETAILS = 'Anomaly score'; + +export const CUSTOM_QUERY_DETAILS = 'Custom query'; + export const DEFINITION_DETAILS = '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]'; +export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; + +export const DETAILS_TITLE = '.euiDescriptionList__title'; + +export const FALSE_POSITIVES_DETAILS = 'False positive examples'; + +export const INDEX_PATTERNS_DETAILS = 'Index patterns'; + export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; export const INVESTIGATION_NOTES_TOGGLE = 1; @@ -28,11 +36,38 @@ export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]' export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus"]'; +export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK'; + export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; +export const RULE_NAME_OVERRIDE_DETAILS = 'Rule name override'; + +export const RISK_SCORE_DETAILS = 'Risk score'; + +export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override'; + +export const REFERENCE_URLS_DETAILS = 'Reference URLs'; + +export const RULE_TYPE_DETAILS = 'Rule type'; + +export const RUNS_EVERY_DETAILS = 'Runs every'; + export const SCHEDULE_DETAILS = '[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]'; export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; + +export const SEVERITY_DETAILS = 'Severity'; + +export const TAGS_DETAILS = 'Tags'; + +export const THRESHOLD_DETAILS = 'Threshold'; + +export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template'; + +export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; + +export const getDetails = (title: string) => + cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index f7ef5a904de990..c846ced2febfdb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -22,8 +22,10 @@ import { OPEN_SELECTED_ALERTS_BTN, MARK_ALERT_IN_PROGRESS_BTN, MARK_SELECTED_ALERTS_IN_PROGRESS_BTN, + ALERT_RISK_SCORE_HEADER, } from '../screens/alerts'; import { REFRESH_BUTTON } from '../screens/security_header'; +import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; export const closeFirstAlert = () => { cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); @@ -81,6 +83,12 @@ export const selectNumberOfAlerts = (numberOfAlerts: number) => { } }; +export const sortRiskScore = () => { + cy.get(ALERT_RISK_SCORE_HEADER).click(); + cy.get(TIMELINE_COLUMN_SPINNER).should('exist'); + cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); +}; + export const investigateFirstAlertInTimeline = () => { cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index dc89a39d082bc6..914566a13a9a97 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -28,6 +28,8 @@ import { IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INPUT, INVESTIGATION_NOTES_TEXTAREA, + LOOK_BACK_INTERVAL, + LOOK_BACK_TIME_TYPE, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, MACHINE_LEARNING_TYPE, @@ -36,13 +38,17 @@ import { MITRE_TACTIC_DROPDOWN, MITRE_TECHNIQUES_INPUT, REFERENCE_URLS_INPUT, + REFRESH_BUTTON, RISK_INPUT, RISK_MAPPING_OVERRIDE_OPTION, RISK_OVERRIDE, RULE_DESCRIPTION_INPUT, RULE_NAME_INPUT, RULE_NAME_OVERRIDE, + RULE_STATUS, RULE_TIMESTAMP_OVERRIDE, + RUNS_EVERY_INTERVAL, + RUNS_EVERY_TIME_TYPE, SCHEDULE_CONTINUE_BUTTON, SCHEDULE_EDIT_TAB, SEVERITY_DROPDOWN, @@ -190,6 +196,13 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const fillScheduleRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { + cy.get(RUNS_EVERY_INTERVAL).clear().type(rule.runsEvery.interval); + cy.get(RUNS_EVERY_TIME_TYPE).select(rule.runsEvery.timeType); + cy.get(LOOK_BACK_INTERVAL).clear().type(rule.lookBack.interval); + cy.get(LOOK_BACK_TIME_TYPE).select(rule.lookBack.timeType); +}; + export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; @@ -251,6 +264,14 @@ export const selectThresholdRuleType = () => { cy.get(THRESHOLD_TYPE).click({ force: true }); }; +export const waitForTheRuleToBeExecuted = async () => { + let status = ''; + while (status !== 'succeeded') { + cy.get(REFRESH_BUTTON).click(); + status = await cy.get(RULE_STATUS).invoke('text').promisify(); + } +}; + export const selectEqlRuleType = () => { cy.get(EQL_TYPE).click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts index 7427104a9d889f..28efc47120d326 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts @@ -21,3 +21,7 @@ export const navigateFromHeaderTo = (page: string) => { export const refreshPage = () => { cy.get(REFRESH_BUTTON).click({ force: true }).invoke('text').should('not.equal', 'Updating'); }; + +export const waitForThePageToBeUpdated = () => { + cy.get(REFRESH_BUTTON).should('not.equal', 'Updating'); +}; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 6d79557fdaa28a..4c9e3bc06037e1 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -11,7 +11,7 @@ "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", "cypress:run": "cypress run --browser chrome --headless --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 ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", - "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", + "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "test:generate": "node scripts/endpoint/resolver_generator" }, "devDependencies": { diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 24e25470feb3b3..68eb93f7e2fe85 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -21,6 +21,7 @@ import { useInitSourcerer, useSourcererScope } from '../../common/containers/sou import { useKibana } from '../../common/lib/kibana'; import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade'; import { useThrottledResizeObserver } from '../../common/components/utils'; const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({ @@ -58,6 +59,12 @@ const HomePageComponent: React.FC = ({ children }) => { const [showTimeline] = useShowTimeline(); const { browserFields, indexPattern, indicesExist } = useSourcererScope(); + // side effect: this will attempt to upgrade the endpoint package if it is not up to date + // this will run when a user navigates to the Security Solution app and when they navigate between + // tabs in the app. This is useful for keeping the endpoint package as up to date as possible until + // a background task solution can be built on the server side. Once a background task solution is available we + // can remove this. + useUpgradeEndpointPackage(); return ( diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts new file mode 100644 index 00000000000000..48f826d1c3a912 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/upgrade.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useEffect } from 'react'; +import { HttpFetchOptions, HttpStart } from 'src/core/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { + epmRouteService, + appRoutesService, + CheckPermissionsResponse, + BulkInstallPackagesResponse, +} from '../../../../../ingest_manager/common'; +import { StartServices } from '../../../types'; +import { useIngestEnabledCheck } from './ingest_enabled'; + +/** + * Requests that the endpoint package be upgraded to the latest version + * + * @param http an http client for sending the request + * @param options an object containing options for the request + */ +const sendUpgradeEndpointPackage = async ( + http: HttpStart, + options: HttpFetchOptions = {} +): Promise => { + return http.post(epmRouteService.getBulkInstallPath(), { + ...options, + body: JSON.stringify({ + packages: ['endpoint'], + }), + }); +}; + +/** + * Checks with the ingest manager if the current user making these requests has the right permissions + * to install the endpoint package. + * + * @param http an http client for sending the request + * @param options an object containing options for the request + */ +const sendCheckPermissions = async ( + http: HttpStart, + options: HttpFetchOptions = {} +): Promise => { + return http.get(appRoutesService.getCheckPermissionsPath(), { + ...options, + }); +}; + +export const useUpgradeEndpointPackage = () => { + const context = useKibana(); + const { allEnabled: ingestEnabled } = useIngestEnabledCheck(); + + useEffect(() => { + const abortController = new AbortController(); + + // cancel any ongoing requests + const abortRequests = () => { + abortController.abort(); + }; + + if (ingestEnabled) { + const signal = abortController.signal; + + (async () => { + try { + // make sure we're a privileged user before trying to install the package + const { success: hasPermissions } = await sendCheckPermissions(context.services.http, { + signal, + }); + + // if we're not a privileged user then return and don't try to check the status of the endpoint package + if (!hasPermissions) { + return abortRequests; + } + + // ignore the response for now since we aren't notifying the user + await sendUpgradeEndpointPackage(context.services.http, { signal }); + } catch (error) { + // Ignore Errors, since this should not hinder the user's ability to use the UI + + // ignore the error that occurs from aborting a request + if (!abortController.signal.aborted) { + // eslint-disable-next-line no-console + console.error(error); + } + } + + return abortRequests; + })(); + } + }, [ingestEnabled, context.services.http]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx index 0ddf4d06fb0fcf..366bd156e0c1b5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx @@ -69,7 +69,9 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) <> - {currentStatus?.status ?? getEmptyTagValue()} + + {currentStatus?.status ?? getEmptyTagValue()} + {currentStatus?.status_date != null && currentStatus?.status != null && ( @@ -84,6 +86,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) )} } > diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 80839545951d55..5c2dfa62e59516 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -14,6 +14,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; +import { BuildRuleMessage } from './rule_messages'; interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; @@ -33,6 +34,7 @@ interface BulkCreateMlSignalsParams { refresh: RefreshTypes; tags: string[]; throttle: string; + buildRuleMessage: BuildRuleMessage; } interface EcsAnomaly extends Anomaly { @@ -85,6 +87,6 @@ export const bulkCreateMlSignals = async ( ): Promise => { const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - - return singleBulkCreate({ ...params, filteredEvents: ecsResults }); + const buildRuleMessage = params.buildRuleMessage; + return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts index bdcddbf2ed21bd..9eee04030a909b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -15,6 +15,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; // used to generate constant Threshold Signals ID when run with the same params const NAMESPACE_ID = '0684ec03-7201-4ee0-8ee0-3a3f6b2479b2'; @@ -40,6 +41,7 @@ interface BulkCreateThresholdSignalsParams { tags: string[]; throttle: string; startedAt: Date; + buildRuleMessage: BuildRuleMessage; } interface FilterObject { @@ -195,6 +197,7 @@ export const bulkCreateThresholdSignals = async ( params.ruleParams.threshold!, params.ruleParams.ruleId ); + const buildRuleMessage = params.buildRuleMessage; - return singleBulkCreate({ ...params, filteredEvents: ecsResults }); + return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 604b4521740457..28225680499607 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -12,6 +12,7 @@ import { singleSearchAfter } from './single_search_after'; import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; interface FindThresholdSignalsParams { from: string; @@ -21,6 +22,7 @@ interface FindThresholdSignalsParams { logger: Logger; filter: unknown; threshold: Threshold; + buildRuleMessage: BuildRuleMessage; } export const findThresholdSignals = async ({ @@ -31,6 +33,7 @@ export const findThresholdSignals = async ({ logger, filter, threshold, + buildRuleMessage, }: FindThresholdSignalsParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -59,5 +62,6 @@ export const findThresholdSignals = async ({ logger, filter, pageSize: 0, + buildRuleMessage, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index d369a91335347c..2df180582a0ace 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -80,6 +80,7 @@ export const searchAfterAndBulkCreate = async ({ // perform search_after with optionally undefined sortId const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ + buildRuleMessage, searchAfterSortId: sortId, index: inputIndexPattern, from: tuple.from.toISOString(), @@ -153,6 +154,7 @@ export const searchAfterAndBulkCreate = async ({ success: bulkSuccess, errors: bulkErrors, } = await singleBulkCreate({ + buildRuleMessage, filteredEvents, ruleParams, services, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index f7b56f42755ab4..a3b37270e50b1a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -257,6 +257,7 @@ export const signalRulesAlertType = ({ enabled, refresh, tags, + buildRuleMessage, }); // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = @@ -295,6 +296,7 @@ export const signalRulesAlertType = ({ logger, filter: esFilter, threshold, + buildRuleMessage, }); const { @@ -323,6 +325,7 @@ export const signalRulesAlertType = ({ enabled, refresh, tags, + buildRuleMessage, }); result = mergeReturns([ result, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index 374b967d1e77f0..b7cc13fd13a01b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -19,7 +19,14 @@ import { import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { buildRuleMessageFactory } from './rule_messages'; +const buildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); describe('singleBulkCreate', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -158,6 +165,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -192,6 +200,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -218,6 +227,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(0); @@ -245,6 +255,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(mockLogger.error).not.toHaveBeenCalled(); @@ -274,6 +285,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(mockLogger.error).toHaveBeenCalled(); expect(errors).toEqual(['[4]: internal server error']); @@ -339,6 +351,7 @@ describe('singleBulkCreate', () => { refresh: false, tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', + buildRuleMessage, }); expect(success).toEqual(true); expect(createdItemsCount).toEqual(1); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index e3c3c940b3225b..759890cc9d0746 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -12,6 +12,7 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString, errorAggregator } from './utils'; import { buildBulkBody } from './build_bulk_body'; +import { BuildRuleMessage } from './rule_messages'; import { Logger } from '../../../../../../../src/core/server'; interface SingleBulkCreateParams { @@ -32,6 +33,7 @@ interface SingleBulkCreateParams { tags: string[]; throttle: string; refresh: RefreshTypes; + buildRuleMessage: BuildRuleMessage; } /** @@ -85,6 +87,7 @@ export interface BulkInsertSignalsResponse { // Bulk Index documents. export const singleBulkCreate = async ({ + buildRuleMessage, filteredEvents, ruleParams, services, @@ -104,9 +107,9 @@ export const singleBulkCreate = async ({ throttle, }: SingleBulkCreateParams): Promise => { filteredEvents.hits.hits = filterDuplicateRules(id, filteredEvents); - logger.debug(`about to bulk create ${filteredEvents.hits.hits.length} events`); + logger.debug(buildRuleMessage(`about to bulk create ${filteredEvents.hits.hits.length} events`)); if (filteredEvents.hits.hits.length === 0) { - logger.debug(`all events were duplicates`); + logger.debug(buildRuleMessage(`all events were duplicates`)); return { success: true, createdItemsCount: 0, errors: [] }; } // index documents after creating an ID based on the @@ -153,21 +156,27 @@ export const singleBulkCreate = async ({ body: bulkBody, }); const end = performance.now(); - logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`); - logger.debug(`took property says bulk took: ${response.took} milliseconds`); + logger.debug( + buildRuleMessage( + `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` + ) + ); + logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`)); const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; const errorCountByMessage = errorAggregator(response, [409]); - logger.debug(`bulk created ${createdItemsCount} signals`); + logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`)); if (duplicateSignalsCount > 0) { - logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`); + logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`)); } if (!isEmpty(errorCountByMessage)) { logger.error( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + buildRuleMessage( + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + ) ); return { errors: Object.keys(errorCountByMessage), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index da81911f07ad9b..7b7c40f0c43550 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -12,7 +12,14 @@ import { import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ShardError } from '../../types'; +import { buildRuleMessageFactory } from './rule_messages'; +const buildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -32,6 +39,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); }); @@ -47,6 +55,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchErrors).toEqual([]); }); @@ -94,6 +103,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchErrors).toEqual(['reason: some reason, type: some type, caused by: some reason']); }); @@ -110,6 +120,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); }); @@ -129,6 +140,7 @@ describe('singleSearchAfter', () => { pageSize: 1, filter: undefined, timestampOverride: undefined, + buildRuleMessage, }) ).rejects.toThrow('Fake Error'); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index f758adb21611cd..3b89a2d79c0d04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -8,6 +8,7 @@ import { performance } from 'perf_hooks'; import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; +import { BuildRuleMessage } from './rule_messages'; import { buildEventsSearchQuery } from './build_events_query'; import { createErrorsFromShard, makeFloatString } from './utils'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -23,6 +24,7 @@ interface SingleSearchAfterParams { pageSize: number; filter: unknown; timestampOverride: TimestampOverrideOrUndefined; + buildRuleMessage: BuildRuleMessage; } // utilize search_after for paging results into bulk. @@ -37,6 +39,7 @@ export const singleSearchAfter = async ({ logger, pageSize, timestampOverride, + buildRuleMessage, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -69,7 +72,7 @@ export const singleSearchAfter = async ({ searchErrors, }; } catch (exc) { - logger.error(`[-] nextSearchAfter threw an error ${exc}`); + logger.error(buildRuleMessage(`[-] nextSearchAfter threw an error ${exc}`)); throw exc; } }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts index 570d2fc9192c0b..5a219304cea18b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts @@ -136,6 +136,29 @@ describe('Index Fields', () => { readFromDocValues: false, esTypes: [], }, + { + aggregatable: true, + category: 'agent', + esTypes: [], + indexes: ['auditbeat'], + name: 'agent.user.name', + readFromDocValues: false, + searchable: true, + type: 'string', + }, + { + aggregatable: true, + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + esTypes: [], + example: 15169, + indexes: ['auditbeat'], + name: 'client.as.number.text', + readFromDocValues: false, + searchable: true, + type: 'string', + }, ]) ); }); @@ -149,6 +172,7 @@ describe('Index Fields', () => { ); expect(fields).toEqual([ { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', @@ -156,11 +180,11 @@ describe('Index Fields', () => { searchable: true, aggregatable: false, readFromDocValues: false, - category: 'base', - indexes: ['auditbeat'], esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -169,11 +193,11 @@ describe('Index Fields', () => { searchable: true, aggregatable: true, readFromDocValues: false, - category: 'base', - indexes: ['auditbeat'], esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -181,12 +205,12 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['auditbeat'], readFromDocValues: true, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', @@ -194,12 +218,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Custom 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', @@ -207,12 +231,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', @@ -220,36 +244,59 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['auditbeat'], readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'agent', description: 'Version of the agent.', example: '6.0.0-rc2', name: 'agent.version', type: 'string', searchable: true, aggregatable: true, + readFromDocValues: false, + esTypes: [], + indexes: ['auditbeat'], + }, + { category: 'agent', + name: 'agent.user.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], indexes: ['auditbeat'], + }, + { + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'client.as.number.text', + type: 'string', + searchable: true, + aggregatable: true, readFromDocValues: false, esTypes: [], + indexes: ['auditbeat'], }, { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', type: 'string', searchable: true, aggregatable: false, - category: 'base', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -257,12 +304,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'base', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -270,24 +317,24 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['filebeat'], readFromDocValues: true, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', name: 'agent.hostname', - searchable: true, type: 'string', + searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Custom 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', @@ -295,36 +342,36 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'agent', description: 'Version of the agent.', example: '6.0.0-rc2', name: 'agent.version', type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['filebeat'], readFromDocValues: false, esTypes: [], + indexes: ['filebeat'], }, { + category: 'base', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', name: '_id', type: 'string', searchable: true, aggregatable: false, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'base', description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', @@ -332,12 +379,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'base', description: 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', @@ -345,12 +392,12 @@ describe('Index Fields', () => { type: 'date', searchable: true, aggregatable: true, - category: 'base', - indexes: ['packetbeat'], readFromDocValues: true, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'agent', description: 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', @@ -358,12 +405,12 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, { + category: 'agent', description: 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', @@ -371,10 +418,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: 'agent', - indexes: ['packetbeat'], readFromDocValues: false, esTypes: [], + indexes: ['packetbeat'], }, ]); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts index 403a9425b221fa..71b641237d6b01 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts @@ -121,8 +121,17 @@ export const createFieldItem = ( indexesAliasIdx: number ): IndexField => { const alias = indexesAlias[indexesAliasIdx]; + const splitIndexName = index.name.split('.'); + const indexName = + splitIndexName[splitIndexName.length - 1] === 'text' + ? splitIndexName.slice(0, splitIndexName.length - 1).join('.') + : index.name; + const beatIndex = fieldsBeat[indexName] ?? {}; + if (isEmpty(beatIndex.category)) { + beatIndex.category = splitIndexName[0]; + } return { - ...(fieldsBeat[index.name] ?? {}), + ...beatIndex, ...index, indexes: [alias], }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts index efb992a868f655..e5c502fe26f2cb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts @@ -48,6 +48,22 @@ export const mockAuditbeatIndexField: FieldDescriptor[] = [ readFromDocValues: false, esTypes: [], }, + { + name: 'agent.user.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'client.as.number.text', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, ]; export const mockFilebeatIndexField: FieldDescriptor[] = [ diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index bc89d9e0c812d1..09d4bf8a03c6bb 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1,5 +1,128 @@ { "properties": { + "canvas": { + "properties": { + "workpads": { + "properties": { + "total": { + "type": "long" + } + } + }, + "pages": { + "properties": { + "total": { + "type": "long" + }, + "per_workpad": { + "properties": { + "avg": { + "type": "float" + }, + "min": { + "type": "long" + }, + "max": { + "type": "long" + } + } + } + } + }, + "elements": { + "properties": { + "total": { + "type": "long" + }, + "per_page": { + "properties": { + "avg": { + "type": "float" + }, + "min": { + "type": "long" + }, + "max": { + "type": "long" + } + } + } + } + }, + "functions": { + "properties": { + "total": { + "type": "long" + }, + "in_use": { + "type": "array", + "items": { + "type": "keyword" + } + }, + "per_element": { + "properties": { + "avg": { + "type": "float" + }, + "min": { + "type": "long" + }, + "max": { + "type": "long" + } + } + } + } + }, + "variables": { + "properties": { + "total": { + "type": "long" + }, + "per_workpad": { + "properties": { + "avg": { + "type": "float" + }, + "min": { + "type": "long" + }, + "max": { + "type": "long" + } + } + } + } + }, + "custom_elements": { + "properties": { + "count": { + "type": "long" + }, + "elements": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "functions_in_use": { + "type": "array", + "items": { + "type": "keyword" + } + } + } + } + } + }, "cloud": { "properties": { "isCloudEnabled": { diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts index 8681204755c363..889d23c5e213de 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -5,3 +5,4 @@ */ export type { SearchResponse7 } from '../../ml/common'; +export { composeValidators, patternValidator } from '../../ml/common'; diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index 2efe0bb25bc686..4ec12a27e1b151 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -16,11 +16,11 @@ ], "configPath": ["xpack", "transform"], "requiredBundles": [ - "ml", "esUiShared", "discover", "kibanaUtils", "kibanaReact", - "savedObjects" + "savedObjects", + "ml" ] } diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 470c42d5de7fa8..8b2fcdc7160802 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -14,16 +14,4 @@ export const useRequest = jest.fn(() => ({ })); // just passing through the reimports -export { - getDataGridSchemaFromKibanaFieldType, - getFieldsFromKibanaIndexPattern, - multiColumnSortFactory, - useDataGrid, - useRenderCellValue, - DataGrid, - EsSorting, - RenderCellValue, - UseDataGridReturnType, - UseIndexDataReturnType, - INDEX_STATUS, -} from '../../../ml/public'; +export { getMlSharedImports } from '../../../ml/public'; diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx index 75fefc99b54580..b4de5ff145a594 100644 --- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useContext } from 'react'; + import { coreMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { MlSharedContext } from './shared_context'; + const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); const dataStart = dataPluginMock.createStartContract(); @@ -26,7 +30,8 @@ const appDependencies = { }; export const useAppDependencies = () => { - return appDependencies; + const ml = useContext(MlSharedContext); + return { ...appDependencies, ml }; }; export const useToastNotifications = () => { diff --git a/x-pack/plugins/transform/public/app/__mocks__/shared_context.ts b/x-pack/plugins/transform/public/app/__mocks__/shared_context.ts new file mode 100644 index 00000000000000..5afe0b26f3502a --- /dev/null +++ b/x-pack/plugins/transform/public/app/__mocks__/shared_context.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 { createContext } from 'react'; + +import { GetMlSharedImportsReturnType } from '../../shared_imports'; + +// This code is a workaround to provide dependencies that are +// loaded dynamically during runtime. +export const MlSharedContext = createContext({} as GetMlSharedImportsReturnType); diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index 704875330fdd23..a23465495aceb7 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -11,6 +11,8 @@ import { ScopedHistory } from 'kibana/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import type { GetMlSharedImportsReturnType } from '../shared_imports'; + export interface AppDependencies { chrome: CoreStart['chrome']; data: DataPublicPluginStart; @@ -23,6 +25,7 @@ export interface AppDependencies { storage: Storage; overlays: CoreStart['overlays']; history: ScopedHistory; + ml: GetMlSharedImportsReturnType; } export const useAppDependencies = () => { diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 507579d3743534..b154e1a5621146 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { composeValidators, patternValidator } from '../../../../ml/public'; +import { composeValidators, patternValidator } from '../../../common/shared_imports'; import { AggName } from '../../../common/types/aggregations'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index 1a97ba7806fef1..3b70521427c676 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -12,7 +12,6 @@ import type { DeleteTransformsRequestSchema, } from '../../../common/api_schemas/delete_transforms'; import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; -import { extractErrorMessage } from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$, TransformListRow } from '../common'; @@ -21,7 +20,11 @@ import { useApi } from './use_api'; import { indexService } from '../services/es_index_service'; export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { - const { http, savedObjects } = useAppDependencies(); + const { + http, + savedObjects, + ml: { extractErrorMessage }, + } = useAppDependencies(); const toastNotifications = useToastNotifications(); const [deleteDestIndex, setDeleteDestIndex] = useState(true); @@ -56,7 +59,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { ); } }, - [savedObjects.client, toastNotifications] + [savedObjects.client, toastNotifications, extractErrorMessage] ); const checkUserIndexPermission = useCallback(async () => { @@ -105,7 +108,10 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { type SuccessCountField = keyof Omit; export const useDeleteTransforms = () => { - const { overlays } = useAppDependencies(); + const { + overlays, + ml: { extractErrorMessage }, + } = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index db9ac1e93633f9..4d752ee65fbfda 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { render, wait } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { CoreSetup } from 'src/core/public'; -import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; +import { getMlSharedImports, UseIndexDataReturnType } from '../../shared_imports'; import { SimpleQuery } from '../common'; @@ -22,6 +22,9 @@ jest.mock('../../shared_imports'); jest.mock('../app_dependencies'); jest.mock('./use_api'); +import { useAppDependencies } from '../__mocks__/app_dependencies'; +import { MlSharedContext } from '../__mocks__/shared_context'; + const query: SimpleQuery = { query_string: { query: '*', @@ -31,22 +34,29 @@ const query: SimpleQuery = { describe('Transform: useIndexData()', () => { test('indexPattern set triggers loading', async (done) => { - const { result, waitForNextUpdate } = renderHook(() => - useIndexData( - ({ - id: 'the-id', - title: 'the-title', - fields: [], - } as unknown) as SearchItems['indexPattern'], - query - ) + const mlShared = await getMlSharedImports(); + const wrapper: FC = ({ children }) => ( + {children} + ); + + const { result, waitForNextUpdate } = renderHook( + () => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ), + { wrapper } ); const IndexObj: UseIndexDataReturnType = result.current; await waitForNextUpdate(); expect(IndexObj.errorMessage).toBe(''); - expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.status).toBe(1); expect(IndexObj.tableItems).toEqual([]); done(); }); @@ -61,7 +71,12 @@ describe('Transform: with useIndexData()', () => { fields: [] as any[], } as SearchItems['indexPattern']; + const mlSharedImports = await getMlSharedImports(); + const Wrapper = () => { + const { + ml: { DataGrid }, + } = useAppDependencies(); const props = { ...useIndexData(indexPattern, { match_all: {} }), copyToClipboard: 'the-copy-to-clipboard-code', @@ -73,7 +88,11 @@ describe('Transform: with useIndexData()', () => { return ; }; - const { getByText } = render(); + const { getByText } = render( + + + + ); // Act // Assert diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index ce233d0cf7caab..6f24017b2274fe 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -13,25 +13,16 @@ import { isFieldHistogramsResponseSchema, } from '../../../common/api_schemas/type_guards'; -import { - getFieldType, - getDataGridSchemaFromKibanaFieldType, - getFieldsFromKibanaIndexPattern, - showDataGridColumnChartErrorMessageToast, - useDataGrid, - useRenderCellValue, - EsSorting, - UseIndexDataReturnType, - INDEX_STATUS, -} from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; +import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; + import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; -import { useToastNotifications } from '../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; export const useIndexData = ( indexPattern: SearchItems['indexPattern'], @@ -39,6 +30,17 @@ export const useIndexData = ( ): UseIndexDataReturnType => { const api = useApi(); const toastNotifications = useToastNotifications(); + const { + ml: { + getFieldType, + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + showDataGridColumnChartErrorMessageToast, + useDataGrid, + useRenderCellValue, + INDEX_STATUS, + }, + } = useAppDependencies(); const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index c51bf7d7e67411..536c1d886e758b 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -18,16 +18,10 @@ import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schem import { dictionaryToArray } from '../../../common/types/common'; import { getNestedProperty } from '../../../common/utils/object_utils'; -import { - formatHumanReadableDateTimeSeconds, - multiColumnSortFactory, - useDataGrid, - RenderCellValue, - UseIndexDataReturnType, - INDEX_STATUS, -} from '../../shared_imports'; +import { RenderCellValue, UseIndexDataReturnType } from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; +import { useAppDependencies } from '../app_dependencies'; import { getPreviewTransformRequestBody, PivotAggsConfigDict, @@ -36,10 +30,10 @@ import { PivotQuery, PivotAggsConfig, } from '../common'; +import { isPivotAggsWithExtendedForm } from '../common/pivot_aggs'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; -import { isPivotAggsWithExtendedForm } from '../common/pivot_aggs'; /** * Checks if the aggregations collection is invalid. @@ -79,6 +73,9 @@ export const usePivotData = ( PreviewMappingsProperties >({}); const api = useApi(); + const { + ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS }, + } = useAppDependencies(); const aggsArr = useMemo(() => dictionaryToArray(aggs), [aggs]); const groupByArr = useMemo(() => dictionaryToArray(groupBy), [groupBy]); @@ -258,7 +255,13 @@ export const usePivotData = ( return cellValue; }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]); + }, [ + pageData, + pagination.pageIndex, + pagination.pageSize, + previewMappingsProperties, + formatHumanReadableDateTimeSeconds, + ]); return { ...dataGrid, diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 0392ecbafa832f..17db745652dbfd 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -8,6 +8,7 @@ import { ManagementAppMountParams } from '../../../../../src/plugins/management/ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { PluginsDependencies } from '../plugin'; +import { getMlSharedImports } from '../shared_imports'; import { AppDependencies } from './app_dependencies'; import { breadcrumbService } from './services/navigation'; @@ -47,6 +48,7 @@ export async function mountManagementSection( storage: localStorage, uiSettings, history, + ml: await getMlSharedImports(), }; const unmountAppCallback = renderApp(element, appDependencies); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx index afb0f49a8c816c..983d36a20e87fb 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; +import { isEqual } from 'lodash'; import React, { memo, FC } from 'react'; import { EuiCodeEditor, EuiFormRow } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 3cfab74f421112..30e8c2b594db7c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -18,9 +18,7 @@ import { EuiSelectOption, } from '@elastic/eui'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; +import { cloneDeep } from 'lodash'; import { useUpdateEffect } from 'react-use'; import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts index 0235cdcf8b5e46..1523a0d9a89f9c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; +import { isEqual } from 'lodash'; import { Dictionary } from '../../../../../../../common/types/common'; import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index 416d34481451e8..d59f99192621cf 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -7,9 +7,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import debounce from 'lodash/debounce'; +import { debounce } from 'lodash'; import { useUpdateEffect } from 'react-use'; import { i18n } from '@kbn/i18n'; import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index d6526fd1db05ee..cf1bfda6128ef7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -30,6 +30,9 @@ import { StepDefineForm } from './step_define_form'; jest.mock('../../../../../shared_imports'); jest.mock('../../../../../app/app_dependencies'); +import { MlSharedContext } from '../../../../../app/__mocks__/shared_context'; +import { getMlSharedImports } from '../../../../../shared_imports'; + const createMockWebStorage = () => ({ clear: jest.fn(), getItem: jest.fn(), @@ -51,6 +54,8 @@ describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async (done) => { // Arrange + const mlSharedImports = await getMlSharedImports(); + const searchItems = { indexPattern: { title: 'the-index-pattern-title', @@ -69,7 +74,9 @@ describe('Transform: ', () => { const { getByText } = render( - + + + ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 10f473074b4d78..8f19db6fc827da 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -25,8 +25,6 @@ import { import { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; -import { DataGrid } from '../../../../../shared_imports'; - import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement, @@ -42,7 +40,7 @@ import { import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { useIndexData } from '../../../../hooks/use_index_data'; import { usePivotData } from '../../../../hooks/use_pivot_data'; -import { useToastNotifications } from '../../../../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AdvancedPivotEditor } from '../advanced_pivot_editor'; @@ -66,6 +64,9 @@ export const StepDefineForm: FC = React.memo((props) => { const { searchItems } = props; const { indexPattern } = searchItems; + const { + ml: { DataGrid }, + } = useAppDependencies(); const toastNotifications = useToastNotifications(); const stepDefineForm = useStepDefineForm(props); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index f8a060e0007b86..7aed0568e6efc3 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -22,10 +22,15 @@ import { StepDefineSummary } from './step_define_summary'; jest.mock('../../../../../shared_imports'); jest.mock('../../../../../app/app_dependencies'); +import { MlSharedContext } from '../../../../../app/__mocks__/shared_context'; +import { getMlSharedImports } from '../../../../../shared_imports'; + describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async (done) => { // Arrange + const mlSharedImports = await getMlSharedImports(); + const searchItems = { indexPattern: { title: 'the-index-pattern-title', @@ -57,7 +62,9 @@ describe('Transform: ', () => { }; const { getByText } = render( - + + + ); // Act diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index c4adb9f1f49dee..24718ffaa937cd 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -12,9 +12,7 @@ import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { dictionaryToArray } from '../../../../../../common/types/common'; -import { DataGrid } from '../../../../../shared_imports'; - -import { useToastNotifications } from '../../../../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, getPivotPreviewDevConsoleStatement, @@ -39,6 +37,9 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const { + ml: { DataGrid }, + } = useAppDependencies(); const toastNotifications = useToastNotifications(); const pivotAggsArr = dictionaryToArray(aggList); const pivotGroupByArr = dictionaryToArray(groupByList); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index b0e1770c50d531..0b1df3d81aa856 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import merge from 'lodash/merge'; +import { isEqual } from 'lodash'; +import { merge } from 'lodash'; import { useReducer } from 'react'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx index 846d8a8ccd2006..6d04c08f8b238b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx @@ -13,9 +13,12 @@ import { ExpandedRow } from './expanded_row'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; import { within } from '@testing-library/dom'; -jest.mock('../../../../../shared_imports', () => ({ - formatHumanReadableDateTimeSeconds: jest.fn(), -})); +jest.mock('../../../../../shared_imports'); +jest.mock('../../../../../app/app_dependencies'); + +import { MlSharedContext } from '../../../../../app/__mocks__/shared_context'; +import { getMlSharedImports } from '../../../../../shared_imports'; + describe('Transform: Transform List ', () => { // Set timezone to US/Eastern for consistent test results. beforeEach(() => { @@ -27,9 +30,14 @@ describe('Transform: Transform List ', () => { }); test('Minimal initialization', async () => { + const mlShared = await getMlSharedImports(); const item: TransformListRow = transformListRow; - const { getByText, getByTestId } = render(); + const { getByText, getByTestId } = render( + + + + ); expect(getByText('Details')).toBeInTheDocument(); expect(getByText('Stats')).toBeInTheDocument(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 15efa46dfb8915..4478edab0dba54 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -11,8 +11,8 @@ import { Optional } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; -import { formatHumanReadableDateTimeSeconds } from '../../../../../shared_imports'; import { TransformListRow } from '../../../../common'; +import { useAppDependencies } from '../../../../app_dependencies'; import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane'; import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; @@ -38,6 +38,9 @@ interface Props { type StateValues = Optional; export const ExpandedRow: FC = ({ item }) => { + const { + ml: { formatHumanReadableDateTimeSeconds }, + } = useAppDependencies(); const stateValues: StateValues = { ...item.stats }; delete stateValues.stats; delete stateValues.checkpointing; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 87d9a25dababdd..84ac646dddc11c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -7,9 +7,8 @@ import React, { useMemo, FC } from 'react'; import { TransformPivotConfig } from '../../../../../../common/types/transform'; -import { DataGrid } from '../../../../../shared_imports'; -import { useToastNotifications } from '../../../../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery } from '../../../../common'; import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; @@ -24,6 +23,9 @@ interface ExpandedRowPreviewPaneProps { } export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { + const { + ml: { DataGrid }, + } = useAppDependencies(); const toastNotifications = useToastNotifications(); const { aggList, groupByList, searchQuery } = useMemo( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts index b3aafbd187c914..9739fb6be6afba 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts @@ -6,12 +6,8 @@ import { useState } from 'react'; import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import sortBy from 'lodash/sortBy'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import get from 'lodash/get'; +import { sortBy } from 'lodash'; +import { get } from 'lodash'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index b977c657b4a5a3..4f29ab3708fc8b 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -9,22 +9,11 @@ export { XJsonMode } from '@kbn/ace'; export { UseRequestConfig, useRequest } from '../../../../src/plugins/es_ui_shared/public'; export { - getFieldType, - extractErrorMessage, - formatHumanReadableDateTimeSeconds, - getDataGridSchemaFromKibanaFieldType, - getFieldsFromKibanaIndexPattern, - multiColumnSortFactory, - showDataGridColumnChartErrorMessageToast, - useDataGrid, - useRenderCellValue, - ChartData, - DataGrid, + getMlSharedImports, + GetMlSharedImportsReturnType, + UseIndexDataReturnType, EsSorting, RenderCellValue, - UseDataGridReturnType, - UseIndexDataReturnType, - INDEX_STATUS, } from '../../ml/public'; import { XJson } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1c2c4c78504f0c..79d86a199e9061 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8986,7 +8986,6 @@ "xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動", "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録", "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード", - "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加", "xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。", @@ -9068,7 +9067,6 @@ "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる", "xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット", "xpack.ingestManager.appNavigation.epmLinkText": "統合", - "xpack.ingestManager.appNavigation.fleetLinkText": "フリート", "xpack.ingestManager.appNavigation.overviewLinkText": "概要", "xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信", "xpack.ingestManager.appNavigation.settingsButton": "設定", @@ -9078,9 +9076,6 @@ "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて", "xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager", "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット", - "xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "エージェント", - "xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "登録トークン", - "xpack.ingestManager.breadcrumbs.fleetPageTitle": "フリート", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要", @@ -9149,8 +9144,6 @@ "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "統合を検索", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です", - "xpack.ingestManager.fleet.pageSubtitle": "構成の更新を管理し、任意のサイズのエージェントのグループにデプロイします。", - "xpack.ingestManager.fleet.pageTitle": "フリート", "xpack.ingestManager.genericActionsMenuText": "開く", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", "xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "Ingest Managerベータを試す", @@ -9229,7 +9222,6 @@ "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。", "xpack.ingestManager.overviewPageEnrollAgentButton": "エージェントの追加", "xpack.ingestManager.overviewPageFleetPanelAction": "エージェントを表示", - "xpack.ingestManager.overviewPageFleetPanelTitle": "フリート", "xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、構成を管理します。", "xpack.ingestManager.overviewPageIntegrationsPanelAction": "統合を表示", "xpack.ingestManager.overviewPageIntegrationsPanelTitle": "統合", @@ -18198,7 +18190,6 @@ "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "不十分なライセンスレベル", - "xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern": "対象インデックスパターンを選択", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", @@ -18206,7 +18197,6 @@ "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", - "xpack.uiActionsEnhanced.drilldown.goToDiscover": "Discoverに移動(例)", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "ドキュメントを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5738b440a9e925..3e45418e28a28f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8992,7 +8992,6 @@ "xpack.ingestManager.agentEnrollment.downloadLink": "前往 elastic.co/downloads", "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "注册到 Fleet", "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "独立模式", - "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "注册代理前需要设置 Fleet。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "添加代理", "xpack.ingestManager.agentEnrollment.managedDescription": "无论是需要一个代理还是需要数以千计的代理,Fleet 允许您轻松地集中管理并部署代理的更新。按照下面的说明下载 Elastic 代理并将代理注册到 Fleet。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "如果希望对以独立模式运行的代理进行配置更改,则需要手动更新。按照下面的说明下载并设置独立模式的 Elastic 代理。", @@ -9074,7 +9073,6 @@ "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "关闭", "xpack.ingestManager.appNavigation.dataStreamsLinkText": "数据集", "xpack.ingestManager.appNavigation.epmLinkText": "集成", - "xpack.ingestManager.appNavigation.fleetLinkText": "Fleet", "xpack.ingestManager.appNavigation.overviewLinkText": "概览", "xpack.ingestManager.appNavigation.sendFeedbackButton": "发送反馈", "xpack.ingestManager.appNavigation.settingsButton": "设置", @@ -9084,9 +9082,6 @@ "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "全部", "xpack.ingestManager.breadcrumbs.appTitle": "采集管理器", "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "数据集", - "xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "代理", - "xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "注册令牌", - "xpack.ingestManager.breadcrumbs.fleetPageTitle": "Fleet", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "已安装", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "集成", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概览", @@ -9155,8 +9150,6 @@ "xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "未找到任何软件包", "xpack.ingestManager.epmList.searchPackagesPlaceholder": "搜索集成", "xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "有可用更新", - "xpack.ingestManager.fleet.pageSubtitle": "管理配置更新并将其部署到一组任意大小的代理。", - "xpack.ingestManager.fleet.pageTitle": "Fleet", "xpack.ingestManager.genericActionsMenuText": "打开", "xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", "xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "试用采集管理器公测版", @@ -9235,7 +9228,6 @@ "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "您的代理收集的数据组织到各种数据集中。", "xpack.ingestManager.overviewPageEnrollAgentButton": "添加代理", "xpack.ingestManager.overviewPageFleetPanelAction": "查看代理", - "xpack.ingestManager.overviewPageFleetPanelTitle": "Fleet", "xpack.ingestManager.overviewPageFleetPanelTooltip": "使用 Fleet 注册代理并从集中位置管理其配置。", "xpack.ingestManager.overviewPageIntegrationsPanelAction": "查看集成", "xpack.ingestManager.overviewPageIntegrationsPanelTitle": "集成", @@ -18209,7 +18201,6 @@ "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "已注册对象类型“{id}”。", "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "许可证级别不够", - "xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern": "选择目标索引模式", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", @@ -18217,7 +18208,6 @@ "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围", "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围", - "xpack.uiActionsEnhanced.drilldown.goToDiscover": "前往 Discover(示例)", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏", "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "查看文档", diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index e048eeed9534a9..272ec3edc9d29f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -7,9 +7,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { ToastsStart } from 'kibana/public'; import useMountedState from 'react-use/lib/useMountedState'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import intersection from 'lodash/intersection'; +import { intersection } from 'lodash'; import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard'; import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts index 4a6de4e26e8a7d..74940c4b070773 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import partition from 'lodash/partition'; +import { partition } from 'lodash'; import { getFlattenedObject } from '@kbn/std'; import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; diff --git a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx new file mode 100644 index 00000000000000..cdc3632545c5c5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Alert, AlertEdit } from '../../../../../../plugins/triggers_actions_ui/public'; + +interface Props { + alertFlyoutVisible: boolean; + initialAlert: Alert; + setAlertFlyoutVisibility: React.Dispatch>; +} + +export const UptimeEditAlertFlyoutComponent = ({ + alertFlyoutVisible, + initialAlert, + setAlertFlyoutVisibility, +}: Props) => { + const onClose = () => { + setAlertFlyoutVisibility(false); + }; + return alertFlyoutVisible ? : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index 7971c4eb583504..c8260721ff84ae 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -22,7 +22,12 @@ import { useMonitorId } from '../../../hooks'; import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../state/actions'; import { useAnomalyAlert } from './use_anomaly_alert'; import { ConfirmAlertDeletion } from './confirm_alert_delete'; -import { deleteAnomalyAlertAction } from '../../../state/alerts/alerts'; +import { + deleteAnomalyAlertAction, + getAnomalyAlertAction, + isAnomalyAlertDeleting, +} from '../../../state/alerts/alerts'; +import { UptimeEditAlertFlyoutComponent } from '../../common/alerts/uptime_edit_alert_flyout'; interface Props { hasMLJob: boolean; @@ -33,11 +38,14 @@ interface Props { export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Props) => { const [isPopOverOpen, setIsPopOverOpen] = useState(false); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const { basePath } = useContext(UptimeSettingsContext); const canDeleteMLJob = useSelector(canDeleteMLJobSelector); const isMLJobCreating = useSelector(isMLJobCreatingSelector); + const isAlertDeleting = useSelector(isAnomalyAlertDeleting); const { loading: isMLJobLoading } = useSelector(hasMLJobSelector); @@ -54,7 +62,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const deleteAnomalyAlert = () => dispatch(deleteAnomalyAlertAction.get({ alertId: anomalyAlert?.id as string })); - const showLoading = isMLJobCreating || isMLJobLoading; + const showLoading = isMLJobCreating || isMLJobLoading || isAlertDeleting; const btnText = hasMLJob ? labels.ANOMALY_DETECTION : labels.ENABLE_ANOMALY_DETECTION; @@ -63,7 +71,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro data-test-subj={hasMLJob ? 'uptimeManageMLJobBtn' : 'uptimeEnableAnomalyBtn'} onClick={hasMLJob ? () => setIsPopOverOpen(true) : onEnableJob} disabled={hasMLJob && !canDeleteMLJob} - isLoading={isMLJobCreating || isMLJobLoading} + isLoading={showLoading} size="s" aria-label={labels.ENABLE_MANAGE_JOB} > @@ -85,21 +93,27 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro dateRange: { from: dateRangeStart, to: dateRangeEnd }, }), }, - { - name: anomalyAlert ? labels.DISABLE_ANOMALY_ALERT : labels.ENABLE_ANOMALY_ALERT, - 'data-test-subj': anomalyAlert - ? 'uptimeDisableAnomalyAlertBtn' - : 'uptimeEnableAnomalyAlertBtn', - icon: , - onClick: () => { - if (anomalyAlert) { - setIsConfirmAlertDeleteOpen(true); - } else { - dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); - dispatch(setAlertFlyoutVisible(true)); - } - }, - }, + ...(anomalyAlert + ? [ + { + name: 'Anomaly alert', + icon: 'bell', + 'data-test-subj': 'uptimeManageAnomalyAlertBtn', + panel: 1, + }, + ] + : [ + { + name: labels.ENABLE_ANOMALY_ALERT, + 'data-test-subj': 'uptimeEnableAnomalyAlertBtn', + icon: 'bell', + onClick: () => { + dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); + dispatch(setAlertFlyoutVisible(true)); + setIsPopOverOpen(false); + }, + }, + ]), { name: labels.DISABLE_ANOMALY_DETECTION, 'data-test-subj': 'uptimeDeleteMLJobBtn', @@ -111,6 +125,27 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro }, ], }, + { + id: 1, + title: 'Anomaly alert', + items: [ + { + name: 'Edit', + 'data-test-subj': 'uptimeEditAnomalyAlertBtn', + onClick: () => { + setIsFlyoutOpen(true); + setIsPopOverOpen(false); + }, + }, + { + name: 'Disable', + 'data-test-subj': 'uptimeDisableAnomalyAlertBtn', + onClick: () => { + setIsConfirmAlertDeleteOpen(true); + }, + }, + ], + }, ]; return ( @@ -138,6 +173,14 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro }} /> )} + { + setIsFlyoutOpen(false); + dispatch(getAnomalyAlertAction.get({ monitorId })); + }} + /> ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx index 1428a7f526fc2b..dd732f0b7e24b5 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx @@ -16,7 +16,7 @@ import { useSelector } from 'react-redux'; import React, { useEffect, useState } from 'react'; import { AnomalyTranslations } from './translations'; import { AlertExpressionPopover } from '../alert_expression_popover'; -import { DEFAULT_SEVERITY, SelectSeverity } from './select_severity'; +import { DEFAULT_SEVERITY, SelectSeverity, SEVERITY_OPTIONS } from './select_severity'; import { monitorIdSelector } from '../../../../state/selectors'; import { getSeverityColor, getSeverityType } from '../../../../../../ml/public'; @@ -40,6 +40,14 @@ export function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) { setAlertParams('severity', severity.val); }, [severity, setAlertParams]); + useEffect(() => { + if (alertParams.severity !== undefined) { + setSeverity(SEVERITY_OPTIONS.find(({ val }) => val === alertParams.severity)!); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index c1f802c2d0c915..c277e87c1ed744 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -25,5 +25,5 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ name, validate: () => ({ errors: {} }), defaultActionMessage, - requiresAppContext: false, + requiresAppContext: true, }); diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index 5273a331025653..aeb81bb413aa7b 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -163,3 +163,4 @@ export const alertsSelector = ({ alerts }: AppState) => alerts.alerts; export const isAlertDeletedSelector = ({ alerts }: AppState) => alerts.alertDeletion; export const anomalyAlertSelector = ({ alerts }: AppState) => alerts.anomalyAlert; +export const isAnomalyAlertDeleting = ({ alerts }: AppState) => alerts.anomalyAlertDeletion.loading; diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 9e1cb1f5872f10..19dd82d617bd96 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -45,6 +45,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./transaction_groups/transaction_charts')); loadTestFile(require.resolve('./transaction_groups/error_rate')); loadTestFile(require.resolve('./transaction_groups/breakdown')); + loadTestFile(require.resolve('./transaction_groups/distribution')); }); describe('Observability overview', function () { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts new file mode 100644 index 00000000000000..6ea050d2eea3c1 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.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 expect from '@kbn/expect'; +import qs from 'querystring'; +import { isEmpty } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + const url = `/api/apm/services/opbeans-java/transaction_groups/distribution?${qs.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: {}, + transactionName: 'APIRestController#stats', + transactionType: 'request', + })}`; + + describe('Transaction groups distribution', () => { + describe('when data is not loaded ', () => { + it('handles empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + + expect(response.body.noHits).to.be(true); + expect(response.body.buckets.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let response: any; + before(async () => { + await esArchiver.load(archiveName); + response = await supertest.get(url); + }); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct metadata', () => { + expect(response.status).to.be(200); + expect(response.body.noHits).to.be(false); + expect(response.body.buckets.length).to.be.greaterThan(0); + }); + + it('returns groups with some hits', () => { + expect(response.body.buckets.some((bucket: any) => bucket.count > 0)).to.be(true); + }); + + it('returns groups with some samples', () => { + expect(response.body.buckets.some((bucket: any) => !isEmpty(bucket.samples))).to.be(true); + }); + + it('returns the correct number of buckets', () => { + expectSnapshot(response.body.buckets.length).toMatchInline(`19`); + }); + + it('returns the correct bucket size', () => { + expectSnapshot(response.body.bucketSize).toMatchInline(`1000`); + }); + + it('returns the correct buckets', () => { + const bucketWithSamples = response.body.buckets.find( + (bucket: any) => !isEmpty(bucket.samples) + ); + + expectSnapshot(bucketWithSamples.count).toMatchInline(`2`); + + expectSnapshot(bucketWithSamples.samples.sort((sample: any) => sample.traceId)) + .toMatchInline(` + Array [ + Object { + "traceId": "a1333547d1257c636154290cddd38c3a", + "transactionId": "3e656b390989133d", + }, + Object { + "traceId": "c799c34f4ee2b0f9998745ea7354d599", + "transactionId": "69b6251b239abb46", + }, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 6364a79a12f045..5825c8fc49a6bb 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -59,13 +59,13 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) 0, ], "fcp": 1072, - "fid": "1.35", + "fid": 1352.13, "fidRanks": Array [ 0, 0, 100, ], - "lcp": "1.27", + "lcp": 1270.5, "lcpRanks": Array [ 100, 0, diff --git a/x-pack/test/functional/apps/uptime/ml_anomaly.ts b/x-pack/test/functional/apps/uptime/ml_anomaly.ts index 20491a063caf84..69309969218239 100644 --- a/x-pack/test/functional/apps/uptime/ml_anomaly.ts +++ b/x-pack/test/functional/apps/uptime/ml_anomaly.ts @@ -40,7 +40,6 @@ export default ({ getService }: FtrProviderContext) => { it('can create job successfully', async () => { await uptime.ml.createMLJob(); - // await uptime.navigation.refreshApp(); }); it('can open ML Manage Menu', async () => { diff --git a/x-pack/test/functional/services/uptime/ml_anomaly.ts b/x-pack/test/functional/services/uptime/ml_anomaly.ts index ac9f6ab2b3d147..cdeec2129e459f 100644 --- a/x-pack/test/functional/services/uptime/ml_anomaly.ts +++ b/x-pack/test/functional/services/uptime/ml_anomaly.ts @@ -66,8 +66,8 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) { return await testSubjects.click('uptimeEnableAnomalyAlertBtn'); }, - async disableAnomalyAlertIsVisible() { - return await testSubjects.exists('uptimeDisableAnomalyAlertBtn'); + async manageAnomalyAlertIsVisible() { + return await testSubjects.exists('uptimeManageAnomalyAlertBtn'); }, async changeAlertThreshold(level: string) { diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts index 03343bff642c3c..55ef7e9784ff49 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts @@ -111,7 +111,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('change button to disable anomaly alert', async () => { await uptime.ml.openMLManageMenu(); - expect(uptime.ml.disableAnomalyAlertIsVisible()).to.eql(true); + expect(uptime.ml.manageAnomalyAlertIsVisible()).to.eql(true); }); it('can delete job successfully', async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts index e377ea5a762f95..bafcb79a419c20 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts @@ -10,7 +10,7 @@ import { skipIfNoDockerRegistry } from '../../helpers'; import { BulkInstallPackageInfo, BulkInstallPackagesResponse, - IBulkInstallPackageError, + IBulkInstallPackageHTTPError, } from '../../../../plugins/ingest_manager/common'; export default function (providerContext: FtrProviderContext) { @@ -68,7 +68,7 @@ export default function (providerContext: FtrProviderContext) { expect(entry.oldVersion).equal('0.1.0'); expect(entry.newVersion).equal('0.3.0'); - const err = body.response[1] as IBulkInstallPackageError; + const err = body.response[1] as IBulkInstallPackageHTTPError; expect(err.statusCode).equal(404); expect(body.response[1].name).equal('blahblah'); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index e509babc9828b6..0cb998b9b7c35e 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -7,6 +7,7 @@ export default function loadTests({ loadTestFile }) { describe('EPM Endpoints', () => { loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts new file mode 100644 index 00000000000000..da06f49dd61397 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/setup.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { GetInfoResponse, Installed } from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const log = getService('log'); + + describe('setup api', async () => { + skipIfNoDockerRegistry(providerContext); + describe('setup performs upgrades', async () => { + const oldEndpointVersion = '0.13.0'; + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('upgrades the endpoint package from 0.13.0 to the latest version available', async function () { + let { body }: { body: GetInfoResponse } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${oldEndpointVersion}`) + .expect(200); + const latestEndpointVersion = body.response.latestVersion; + log.info(`Endpoint package latest version: ${latestEndpointVersion}`); + // make sure we're actually doing an upgrade + expect(latestEndpointVersion).not.eql(oldEndpointVersion); + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + ({ body } = await supertest + .get(`/api/ingest_manager/epm/packages/endpoint-${latestEndpointVersion}`) + .expect(200)); + expect(body.response).to.have.property('savedObject'); + expect((body.response as Installed).savedObject.attributes.install_version).to.eql( + latestEndpointVersion + ); + }); + }); + }); +} diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json index 542c8358114f40..1eb524e71da19a 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "uuid": "3.3.2", - "stats-lite": "2.2.0", + "stats-lite": "^2.2.0", "pretty-ms": "5.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 7aa88bcc883487..b6516b8bf3b556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@babel/cli@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.5.tgz#57df2987c8cf89d0fc7d4b157ec59d7619f1b77a" - integrity sha512-j9H9qSf3kLdM0Ao3aGPbGZ73mEA9XazuupcS6cDGWuiyAcANoguhP0r2Lx32H5JGw4sSSoHG3x/mxVnHgvOoyA== + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.11.6.tgz#1fcbe61c2a6900c3539c06ee58901141f3558482" + integrity sha512-+w7BZCvkewSmaRM6H4L2QM3RL90teqEIHDIFXAmrW33+0jhlymnDAEdqVeCZATvxhQuio1ifoGVlJJbIiH9Ffg== dependencies: commander "^4.0.1" convert-source-map "^1.1.0" @@ -1212,28 +1212,6 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@0.0.55": - version "0.0.55" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.55.tgz#95ff5b70ecb1d333e1a5570a0e4a4079fdf7887b" - integrity sha512-U+YElZOnWEHcLyyfN4hk4LB3znuIR5V6VruPisMXymu/seKrvND2pJmMaB0nP8a1SVa8KufboXrGEDzBWJ4TUg== - dependencies: - classnames "^2.2.5" - core-js "^2.5.1" - focus-trap-react "^3.0.4" - highlight.js "^9.12.0" - html "^1.0.0" - keymirror "^0.1.1" - lodash "^3.10.1" - numeral "^2.0.6" - prop-types "^15.6.0" - react-ace "^5.5.0" - react-color "^2.13.8" - react-datepicker v1.4.1 - react-input-autosize "^2.2.1" - react-virtualized "^9.18.5" - tabbable "^1.1.0" - uuid "^3.1.0" - "@elastic/eui@29.0.0": version "29.0.0" resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-29.0.0.tgz#1c8d822c62ad5e29298a3a36f5b02fd9b32a5550" @@ -3888,7 +3866,7 @@ "@types/glob" "*" "@types/node" "*" -"@types/glob@*", "@types/glob@^5.0.35": +"@types/glob@*": version "5.0.35" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" integrity sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg== @@ -3906,12 +3884,13 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/globby@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/globby/-/globby-6.1.0.tgz#7c25b975512a89effea2a656ca8cf6db7fb29d11" - integrity sha512-j3XSDNoK4LO5T+ZviQD6PqfEjm07QFEacOTbJR3hnLWuWX0ZMLJl9oRPgj1PyrfGbXhfHFkksC9QZ9HFltJyrw== +"@types/glob@^7.1.2": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== dependencies: - "@types/glob" "*" + "@types/minimatch" "*" + "@types/node" "*" "@types/globby@^8.0.0": version "8.0.0" @@ -4056,14 +4035,6 @@ dependencies: "@types/hapi" "*" -"@types/inquirer@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" - integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== - dependencies: - "@types/through" "*" - rxjs "^6.4.0" - "@types/inquirer@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" @@ -4492,10 +4463,10 @@ resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" integrity sha512-ra8IchO9odGQmYKbm+94K58UyKCEKdZh9y0vxhG4pIpOJOBlC1C+ZtBVr6jLs+/oJ4pl+1p/4t3JtBA8J10Vvw== -"@types/pngjs@^3.3.2": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4" - integrity sha512-/SBsv93rVnjByzcau24rBwb+N7BHFp2LateaXz1e7m7M0Wzck/ymXTNdWVrCtkuMbwTHAnfdc3X/I/5szsTEAA== +"@types/pngjs@^3.4.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.4.2.tgz#8dc49b45fbcf18a5873179e3664f049388e39ecf" + integrity sha512-LJVPDraJ5YFEnMHnzxTN4psdWz1M61MtaAAWPn3qnDk5fvs7BAmmQ9pd3KPlrdrvozMyne4ktanD4pg0L7x1Pw== dependencies: "@types/node" "*" @@ -5432,13 +5403,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-2.0.3.tgz#b174827a732efadff81227ed4b8d1cc569baf20a" - integrity sha512-EPSq5wr2aFyAZ1PejJB32IX9Qd4Nwus+adnp7STYFM5/23nLPBazqZ1oor6ZqbH+4otaaGXTlC8RN5hq3C8w9Q== - dependencies: - event-target-shim "^5.0.0" - abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -5753,7 +5717,7 @@ angular-route@^1.8.0: resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.8.0.tgz#cb8066c5d34284ffd6a15ac7be1b3d51c5ad7bb2" integrity sha512-ORvXAdVfCCA6XFwyjSkVQFFGufj0mNGiCvBR93Qsii8+7t/6Ioy6wheUoCj1x4NWUv7hAq3nYYGCVO6QEKb1BQ== -angular-sanitize@1.8.0, angular-sanitize@^1.8.0: +angular-sanitize@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.8.0.tgz#9f80782d3afeec3bcc0bb92b3ca6f1f421cfbca6" integrity sha512-j5GiOPCvfcDWK5svEOVoPb11X3UDVy/mdHPRWuy14Iyw86xaq+Bb+x/em2sAOa5MQQeY5ciLXbF3RRp8iCKcNg== @@ -6244,7 +6208,7 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@3.1.1, archiver@^3.1.1: +archiver@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0" integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg== @@ -8667,7 +8631,7 @@ class-utils@^0.3.5: lazy-cache "^2.0.2" static-extend "^0.1.1" -classnames@2.2.6, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: +classnames@2.2.6, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -9172,11 +9136,6 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== - commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -9192,10 +9151,10 @@ commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== -commander@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" - integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== commander@^4.0.1, commander@^4.1.1: version "4.1.1" @@ -9588,7 +9547,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -11113,11 +11072,6 @@ dom-converter@~0.2: dependencies: utila "~0.4" -"dom-helpers@^2.4.0 || ^3.0.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" - integrity sha512-2Sm+JaYn74OiTM2wHvxJOo3roiq/h25Yi69Fqk269cNUwIXsCvATB6CRSFC9Am/20G2b28hGv/+7NiWydIrPvg== - dom-helpers@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" @@ -13374,7 +13328,7 @@ focus-lock@^0.7.0: resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a" integrity sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw== -focus-trap-react@^3.0.4, focus-trap-react@^3.1.1: +focus-trap-react@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-3.1.2.tgz#4dd021ccd028bbd3321147d132cdf7585d6d1394" integrity sha512-MoQmONoy9gRPyrC5DGezkcOMGgx7MtIOAQDHe098UtL2sA2vmucJwEmQisb+8LRXNYFHxuw5zJ1oLFeKu4Mteg== @@ -13935,11 +13889,6 @@ getobject@~0.1.0: resolved "https://registry.yarnpkg.com/getobject/-/getobject-0.1.0.tgz#047a449789fa160d018f5486ed91320b6ec7885c" integrity sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw= -getopts@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.4.tgz#3137fe8a5fddf304904059a851bdc1c22f0f54fb" - integrity sha512-Rz7DGyomZjrenu9Jx4qmzdlvJgvrEFHXHvjK0FcZtcTC1U5FmES7OdZHUwMuSnEE6QvBvwse1JODKj7TgbSEjQ== - getopts@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" @@ -14007,7 +13956,7 @@ gl-matrix@^3.2.1: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== -glob-all@^3.1.0, glob-all@^3.2.1: +glob-all@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.2.1.tgz#082ca81afd2247cbd3ed2149bb2630f4dc877d95" integrity sha512-x877rVkzB3ipid577QOp+eQCR6M5ZyiwrtaYgrX/z3EThaSPFtLDwBXFHc3sH1cG0R0vFYI5SRYeWMMSEyXkUw== @@ -15385,7 +15334,17 @@ history-extra@^5.0.1: resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== -history@4.9.0, history@^4.9.0: +history@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c" + integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw= + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + query-string "^4.2.2" + warning "^3.0.0" + +history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== @@ -15397,16 +15356,6 @@ history@4.9.0, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^0.4.0" -history@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c" - integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw= - dependencies: - invariant "^2.2.1" - loose-envify "^1.2.0" - query-string "^4.2.2" - warning "^3.0.0" - hjson@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.1.tgz#20de41dc87fc9a10d1557d0230b0e02afb1b09ac" @@ -15541,7 +15490,7 @@ html-webpack-plugin@^4.0.0-beta.2: tapable "^1.1.0" util.promisify "1.0.0" -html@1.0.0, html@^1.0.0: +html@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/html/-/html-1.0.0.tgz#a544fa9ea5492bfb3a2cca8210a10be7b5af1f61" integrity sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E= @@ -18359,7 +18308,7 @@ kew@~0.1.7: resolved "https://registry.yarnpkg.com/kew/-/kew-0.1.7.tgz#0a32a817ff1a9b3b12b8c9bacf4bc4d679af8e72" integrity sha1-CjKoF/8amzsSuMm6z0vE1nmvjnI= -keymirror@0.1.1, keymirror@^0.1.1: +keymirror@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" integrity sha1-kYiJ6hP40KQufFVyUO7nE63JXDU= @@ -19180,11 +19129,6 @@ lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= - log-ok@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" @@ -19367,14 +19311,6 @@ lowlight@~1.9.1: dependencies: highlight.js "~9.12.0" -lru-cache@4.1.5, lru-cache@^4.0.1, lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@4.1.x, lru-cache@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" @@ -19383,6 +19319,14 @@ lru-cache@4.1.x, lru-cache@^4.0.0: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^4.0.1, lru-cache@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -20044,20 +19988,20 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.5.5" tiny-warning "^1.0.3" -mini-css-extract-plugin@0.7.0, mini-css-extract-plugin@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" - integrity sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ== +mini-css-extract-plugin@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" + integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" schema-utils "^1.0.0" webpack-sources "^1.1.0" -mini-css-extract-plugin@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" - integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== +mini-css-extract-plugin@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" + integrity sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" @@ -20365,15 +20309,10 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - -moment@^2.19.3, moment@^2.27.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== +"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.19.3, moment@^2.24.0, moment@^2.27.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75" + integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw== monaco-editor@~0.17.0: version "0.17.1" @@ -20490,16 +20429,11 @@ murmurhash3js@3.0.1: resolved "https://registry.yarnpkg.com/murmurhash3js/-/murmurhash3js-3.0.1.tgz#3e983e5b47c2a06f43a713174e7e435ca044b998" integrity sha1-Ppg+W0fCoG9DpxMXTn5DXKBEuZg= -mustache@2.3.2: +mustache@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" integrity sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ== -mustache@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0" - integrity sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA= - mutation-observer@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.3.tgz#42e9222b101bca82e5ba9d5a7acf4a14c0f263d0" @@ -20685,7 +20619,7 @@ next-tick@1: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -ngreact@0.5.1, ngreact@^0.5.1: +ngreact@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/ngreact/-/ngreact-0.5.1.tgz#2dcccc1541771796689d13e51bb8d5010af41c57" integrity sha512-u/jOWS0KF/twS09O+yuBgNNEEytEhrmSfLTewAuglDSfEYru6a4I8tUnU4fs9/WvlRWbvJTk7WEnwbGamM+Kvg== @@ -20753,7 +20687,7 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-fetch@2.1.2, node-fetch@2.6.1, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.1.2, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -20937,7 +20871,7 @@ node-releases@^1.1.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== -node-sass@^4.13.0, node-sass@^4.13.1: +node-sass@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== @@ -21050,11 +20984,6 @@ normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@3.0.0, normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -21062,6 +20991,11 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -22593,16 +22527,16 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== -pngjs@3.4.0, pngjs@^3.3.3, pngjs@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== - pngjs@^3.0.0: version "3.3.1" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.1.tgz#8e14e6679ee7424b544334c3b2d21cea6d8c209a" integrity sha512-ggXCTsqHRIsGMkHlCEhbHhUmNTA2r1lpkE0NL4Q9S8spkXbm4vE9TVmPso2AGYn90Gltdz8W5CyzhcIGg2Gejg== +pngjs@^3.3.3, pngjs@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + pnp-webpack-plugin@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.5.0.tgz#62a1cd3068f46d564bb33c56eb250e4d586676eb" @@ -22630,16 +22564,16 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.4.5" -popper.js@^1.14.1, popper.js@^1.14.7: - version "1.15.0" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" - integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== - popper.js@^1.14.4: version "1.14.7" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== +popper.js@^1.14.7: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== + portfinder@^1.0.26: version "1.0.27" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.27.tgz#a41333c116b5e5f3d380f9745ac2f35084c4c758" @@ -23405,7 +23339,7 @@ raw-body@~1.1.0: bytes "1" string_decoder "0.10" -raw-loader@3.1.0, raw-loader@^3.1.0: +raw-loader@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f" integrity sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA== @@ -23451,16 +23385,6 @@ re2@^1.15.4: nan "^2.14.1" node-gyp "^7.0.0" -react-ace@^5.5.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.10.0.tgz#e328b37ac52759f700be5afdb86ada2f5ec84c5e" - integrity sha512-aEK/XZCowP8IXq91e2DYqOtGhabk1bbjt+fyeW0UBcIkzDzP/RX/MeJKeyW7wsZcwElACVwyy9nnwXBTqgky3A== - dependencies: - brace "^0.11.0" - lodash.get "^4.4.2" - lodash.isequal "^4.1.1" - prop-types "^15.5.8" - react-ace@^5.9.0: version "5.9.0" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.9.0.tgz#427a1cc4869b960a6f9748aa7eb169a9269fc336" @@ -23545,16 +23469,6 @@ react-color@^2.17.0: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-datepicker@v1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-1.4.1.tgz#ee171b71d9853e56f9eece5fc3186402f4648683" - integrity sha512-O/ExTWLS81pyWJWLFg1BRQEr9S/BDd6iMEkGctxQmVrRw2srW8DNdnQm5UgFNu8LoSZGMDvI55OghYZvDpWJhw== - dependencies: - classnames "^2.2.5" - prop-types "^15.6.0" - react-onclickoutside "^6.7.1" - react-popper "^0.9.1" - react-datetime@^2.14.0: version "2.15.0" resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.15.0.tgz#a8f7da6c58b6b45dbeea32d4e8485db17614e12c" @@ -23596,15 +23510,6 @@ react-dev-utils@^9.0.0: strip-ansi "5.2.0" text-table "0.2.0" -react-docgen-typescript-loader@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.1.0.tgz#09cacf872617c97f946ee920d2239f51d543be41" - integrity sha512-gY+b7RkRPty5ZN4NMQ+jwx9MzTVuIj6LJCwdWRAi1+nrHJfH2gMMytQfxFdzQ7BlgD4COWnSE8Ixtl2L62kCRw== - dependencies: - "@webpack-contrib/schema-utils" "^1.0.0-beta.0" - loader-utils "^1.2.3" - react-docgen-typescript "^1.12.3" - react-docgen-typescript-loader@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.1.1.tgz#c1992538524fb9e45246d6c1314ddcfbf26e9d08" @@ -23747,13 +23652,6 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" -react-input-autosize@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" - integrity sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA== - dependencies: - prop-types "^15.5.8" - react-input-autosize@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" @@ -23853,11 +23751,6 @@ react-onclickoutside@^6.5.0: resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93" integrity sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg== -react-onclickoutside@^6.7.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.8.0.tgz#9f91b5b3ed59f4d9e43fd71620dc200773a4d569" - integrity sha512-5Q4Rn7QLEoh7WIe66KFvYIpWJ49GeHoygP1/EtJyZjXKgrWH19Tf0Ty3lWyQzrEEDyLOwUvvmBFSE3dcDdvagA== - react-popper-tooltip@^2.10.1, react-popper-tooltip@^2.8.3: version "2.11.1" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz#3c4bdfd8bc10d1c2b9a162e859bab8958f5b2644" @@ -23866,14 +23759,6 @@ react-popper-tooltip@^2.10.1, react-popper-tooltip@^2.8.3: "@babel/runtime" "^7.9.2" react-popper "^1.3.7" -react-popper@^0.9.1: - version "0.9.5" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.9.5.tgz#02a24ef3eec33af9e54e8358ab70eb0e331edd05" - integrity sha1-AqJO8+7DOvnlToNYq3DrDjMe3QU= - dependencies: - popper.js "^1.14.1" - prop-types "^15.6.1" - react-popper@^1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" @@ -24149,18 +24034,6 @@ react-virtualized-auto-sizer@^1.0.2: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== -react-virtualized@^9.18.5: - version "9.20.1" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d" - integrity sha512-xIWxBsyNAjceqD3hsE0nw5TcDVxKbIepsHhvS2XneHmNz0KlKxdLdGBmGZBM9ZesEmbZ5EO0Sw70TB1MeCmpbQ== - dependencies: - babel-runtime "^6.26.0" - classnames "^2.2.3" - dom-helpers "^2.4.0 || ^3.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" - react-virtualized@^9.21.2: version "9.21.2" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" @@ -24584,11 +24457,6 @@ redux-saga@^1.1.3: dependencies: "@redux-saga/core" "^1.1.3" -redux-thunk@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" - integrity sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU= - redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -25523,13 +25391,6 @@ rxjs-marbles@^5.0.6: dependencies: fast-equals "^2.0.0" -rxjs@6.5.5, rxjs@^6.5.5: - version "6.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" - integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== - dependencies: - tslib "^1.9.0" - rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" @@ -25544,6 +25405,13 @@ rxjs@^6.1.0, rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1: dependencies: tslib "^1.9.0" +rxjs@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== + dependencies: + tslib "^1.9.0" + rxjs@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" @@ -25835,11 +25703,6 @@ semver@5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -semver@5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -26180,13 +26043,6 @@ simple-git@1.116.0: dependencies: debug "^4.0.1" -simple-git@^1.91.0: - version "1.92.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.92.0.tgz#6061468eb7d19f0141078fc742e62457e910f547" - integrity sha1-YGFGjrfRnwFBB4/HQuYkV+kQ9Uc= - dependencies: - debug "^3.1.0" - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -27469,7 +27325,7 @@ symbol.prototype.description@^1.0.0: dependencies: has-symbols "^1.0.0" -tabbable@1.1.3, tabbable@^1.1.0: +tabbable@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" integrity sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg== @@ -28004,13 +27860,6 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.1.0, tmp@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== - dependencies: - rimraf "^2.6.3" - tmp@^0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" @@ -29079,7 +28928,7 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-loader@2.2.0, url-loader@^2.0.1: +url-loader@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.2.0.tgz#af321aece1fd0d683adc8aaeb27829f29c75b46e" integrity sha512-G8nk3np8ZAnwhHXas1JxJEwJyQdqFXAKJehfgZ/XrC48volFBRtO+FIKtF2u0Ma3bw+4vnDVjHPAQYlF9p2vsw== @@ -30959,7 +30808,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.10.0, yauzl@^2.10.0: +yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=